• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import abc
6import datetime
7import difflib
8import functools
9import hashlib
10import logging
11import operator
12import os
13import re
14import sys
15import warnings
16
17import common
18
19from autotest_lib.frontend.afe.json_rpc import proxy
20from autotest_lib.client.common_lib import control_data
21from autotest_lib.client.common_lib import enum
22from autotest_lib.client.common_lib import error
23from autotest_lib.client.common_lib import global_config
24from autotest_lib.client.common_lib import priorities
25from autotest_lib.client.common_lib import time_utils
26from autotest_lib.client.common_lib import utils
27from autotest_lib.frontend.afe.json_rpc import proxy
28from autotest_lib.server.cros import provision
29from autotest_lib.server.cros.dynamic_suite import constants
30from autotest_lib.server.cros.dynamic_suite import control_file_getter
31from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
32from autotest_lib.server.cros.dynamic_suite import job_status
33from autotest_lib.server.cros.dynamic_suite import tools
34from autotest_lib.server.cros.dynamic_suite.job_status import Status
35
36try:
37    from chromite.lib import boolparse_lib
38    from chromite.lib import cros_logging as logging
39except ImportError:
40    print 'Unable to import chromite.'
41    print 'This script must be either:'
42    print '  - Be run in the chroot.'
43    print '  - (not yet supported) be run after running '
44    print '    ../utils/build_externals.py'
45
46_FILE_BUG_SUITES = ['au', 'bvt', 'bvt-cq', 'bvt-inline', 'paygen_au_beta',
47                    'paygen_au_canary', 'paygen_au_dev', 'paygen_au_stable',
48                    'sanity', 'push_to_prod']
49_AUTOTEST_DIR = global_config.global_config.get_config_value(
50        'SCHEDULER', 'drone_installation_directory')
51ENABLE_CONTROLS_IN_BATCH = global_config.global_config.get_config_value(
52        'CROS', 'enable_getting_controls_in_batch', type=bool, default=False)
53
54class RetryHandler(object):
55    """Maintain retry information.
56
57    @var _retry_map: A dictionary that stores retry history.
58            The key is afe job id. The value is a dictionary.
59            {job_id: {'state':RetryHandler.States, 'retry_max':int}}
60            - state:
61                The retry state of a job.
62                NOT_ATTEMPTED:
63                    We haven't done anything about the job.
64                ATTEMPTED:
65                    We've made an attempt to schedule a retry job. The
66                    scheduling may or may not be successful, e.g.
67                    it might encounter an rpc error. Note failure
68                    in scheduling a retry is different from a retry job failure.
69                    For each job, we only attempt to schedule a retry once.
70                    For example, assume we have a test with JOB_RETRIES=5 and
71                    its second retry job failed. When we attempt to create
72                    a third retry job to retry the second, we hit an rpc
73                    error. In such case, we will give up on all following
74                    retries.
75                RETRIED:
76                    A retry job has already been successfully
77                    scheduled.
78            - retry_max:
79                The maximum of times the job can still
80                be retried, taking into account retries
81                that have occurred.
82    @var _retry_level: A retry might be triggered only if the result
83            is worse than the level.
84    @var _max_retries: Maximum retry limit at suite level.
85                     Regardless how many times each individual test
86                     has been retried, the total number of retries happening in
87                     the suite can't exceed _max_retries.
88    """
89
90    States = enum.Enum('NOT_ATTEMPTED', 'ATTEMPTED', 'RETRIED',
91                       start_value=1, step=1)
92
93    def __init__(self, initial_jobs_to_tests, retry_level='WARN',
94                 max_retries=None):
95        """Initialize RetryHandler.
96
97        @param initial_jobs_to_tests: A dictionary that maps a job id to
98                a ControlData object. This dictionary should contain
99                jobs that are originally scheduled by the suite.
100        @param retry_level: A retry might be triggered only if the result is
101                worse than the level.
102        @param max_retries: Integer, maxmium total retries allowed
103                                  for the suite. Default to None, no max.
104        """
105        self._retry_map = {}
106        self._retry_level = retry_level
107        self._max_retries = (max_retries
108                             if max_retries is not None else sys.maxint)
109        for job_id, test in initial_jobs_to_tests.items():
110            if test.job_retries > 0:
111                self._add_job(new_job_id=job_id,
112                              retry_max=test.job_retries)
113
114
115    def _add_job(self, new_job_id, retry_max):
116        """Add a newly-created job to the retry map.
117
118        @param new_job_id: The afe_job_id of a newly created job.
119        @param retry_max: The maximum of times that we could retry
120                          the test if the job fails.
121
122        @raises ValueError if new_job_id is already in retry map.
123
124        """
125        if new_job_id in self._retry_map:
126            raise ValueError('add_job called when job is already in retry map.')
127
128        self._retry_map[new_job_id] = {
129                'state': self.States.NOT_ATTEMPTED,
130                'retry_max': retry_max}
131
132
133    def _suite_max_reached(self):
134        """Return whether maximum retry limit for a suite has been reached."""
135        return self._max_retries <= 0
136
137
138    def add_retry(self, old_job_id, new_job_id):
139        """Record a retry.
140
141        Update retry map with the retry information.
142
143        @param old_job_id: The afe_job_id of the job that is retried.
144        @param new_job_id: The afe_job_id of the retry job.
145
146        @raises KeyError if old_job_id isn't in the retry map.
147        @raises ValueError if we have already retried or made an attempt
148                to retry the old job.
149
150        """
151        old_record = self._retry_map[old_job_id]
152        if old_record['state'] != self.States.NOT_ATTEMPTED:
153            raise ValueError(
154                    'We have already retried or attempted to retry job %d' %
155                    old_job_id)
156        old_record['state'] = self.States.RETRIED
157        self._add_job(new_job_id=new_job_id,
158                      retry_max=old_record['retry_max'] - 1)
159        self._max_retries -= 1
160
161
162    def set_attempted(self, job_id):
163        """Set the state of the job to ATTEMPTED.
164
165        @param job_id: afe_job_id of a job.
166
167        @raises KeyError if job_id isn't in the retry map.
168        @raises ValueError if the current state is not NOT_ATTEMPTED.
169
170        """
171        current_state = self._retry_map[job_id]['state']
172        if current_state != self.States.NOT_ATTEMPTED:
173            # We are supposed to retry or attempt to retry each job
174            # only once. Raise an error if this is not the case.
175            raise ValueError('Unexpected state transition: %s -> %s' %
176                             (self.States.get_string(current_state),
177                              self.States.get_string(self.States.ATTEMPTED)))
178        else:
179            self._retry_map[job_id]['state'] = self.States.ATTEMPTED
180
181
182    def has_following_retry(self, result):
183        """Check whether there will be a following retry.
184
185        We have the following cases for a given job id (result.id),
186        - no retry map entry -> retry not required, no following retry
187        - has retry map entry:
188            - already retried -> has following retry
189            - has not retried
190                (this branch can be handled by checking should_retry(result))
191                - retry_max == 0 --> the last retry job, no more retry
192                - retry_max > 0
193                   - attempted, but has failed in scheduling a
194                     following retry due to rpc error  --> no more retry
195                   - has not attempped --> has following retry if test failed.
196
197        @param result: A result, encapsulating the status of the job.
198
199        @returns: True, if there will be a following retry.
200                  False otherwise.
201
202        """
203        return (result.test_executed
204                and result.id in self._retry_map
205                and (self._retry_map[result.id]['state'] == self.States.RETRIED
206                     or self._should_retry(result)))
207
208
209    def _should_retry(self, result):
210        """Check whether we should retry a job based on its result.
211
212        We will retry the job that corresponds to the result
213        when all of the following are true.
214        a) The test was actually executed, meaning that if
215           a job was aborted before it could ever reach the state
216           of 'Running', the job will not be retried.
217        b) The result is worse than |self._retry_level| which
218           defaults to 'WARN'.
219        c) The test requires retry, i.e. the job has an entry in the retry map.
220        d) We haven't made any retry attempt yet, i.e. state == NOT_ATTEMPTED
221           Note that if a test has JOB_RETRIES=5, and the second time
222           it was retried it hit an rpc error, we will give up on
223           all following retries.
224        e) The job has not reached its retry max, i.e. retry_max > 0
225
226        @param result: A result, encapsulating the status of the job.
227
228        @returns: True if we should retry the job.
229
230        """
231        return (
232            result.test_executed
233            and result.id in self._retry_map
234            and not self._suite_max_reached()
235            and result.is_worse_than(
236                job_status.Status(self._retry_level, '', 'reason'))
237            and self._retry_map[result.id]['state'] == self.States.NOT_ATTEMPTED
238            and self._retry_map[result.id]['retry_max'] > 0
239        )
240
241
242    def get_retry_max(self, job_id):
243        """Get the maximum times the job can still be retried.
244
245        @param job_id: afe_job_id of a job.
246
247        @returns: An int, representing the maximum times the job can still be
248                  retried.
249        @raises KeyError if job_id isn't in the retry map.
250
251        """
252        return self._retry_map[job_id]['retry_max']
253
254
255class _SuiteChildJobCreator(object):
256    """Create test jobs for a suite."""
257
258    def __init__(
259            self,
260            tag,
261            builds,
262            board,
263            afe=None,
264            max_runtime_mins=24*60,
265            timeout_mins=24*60,
266            suite_job_id=None,
267            ignore_deps=False,
268            extra_deps=(),
269            priority=priorities.Priority.DEFAULT,
270            offload_failures_only=False,
271            test_source_build=None,
272            job_keyvals=None):
273        """
274        Constructor
275
276        @param tag: a string with which to tag jobs run in this suite.
277        @param builds: the builds on which we're running this suite.
278        @param board: the board on which we're running this suite.
279        @param afe: an instance of AFE as defined in server/frontend.py.
280        @param max_runtime_mins: Maximum suite runtime, in minutes.
281        @param timeout_mins: Maximum job lifetime, in minutes.
282        @param suite_job_id: Job id that will act as parent id to all sub jobs.
283                             Default: None
284        @param ignore_deps: True if jobs should ignore the DEPENDENCIES
285                            attribute and skip applying of dependency labels.
286                            (Default:False)
287        @param extra_deps: A list of strings which are the extra DEPENDENCIES
288                           to add to each test being scheduled.
289        @param priority: Integer priority level.  Higher is more important.
290        @param offload_failures_only: Only enable gs_offloading for failed
291                                      jobs.
292        @param test_source_build: Build that contains the server-side test code.
293        @param job_keyvals: General job keyvals to be inserted into keyval file,
294                            which will be used by tko/parse later.
295        """
296        self._tag = tag
297        self._builds = builds
298        self._board = board
299        self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
300                                                         delay_sec=10,
301                                                         debug=False)
302        self._max_runtime_mins = max_runtime_mins
303        self._timeout_mins = timeout_mins
304        self._suite_job_id = suite_job_id
305        self._ignore_deps = ignore_deps
306        self._extra_deps = tuple(extra_deps)
307        self._priority = priority
308        self._offload_failures_only = offload_failures_only
309        self._test_source_build = test_source_build
310        self._job_keyvals = job_keyvals
311
312
313    @property
314    def cros_build(self):
315        """Return the CrOS build or the first build in the builds dict."""
316        # TODO(ayatane): Note that the builds dict isn't ordered.  I'm not
317        # sure what the implications of this are, but it's probably not a
318        # good thing.
319        return self._builds.get(provision.CROS_VERSION_PREFIX,
320                                self._builds.values()[0])
321
322
323    def create_job(self, test, retry_for=None):
324        """
325        Thin wrapper around frontend.AFE.create_job().
326
327        @param test: ControlData object for a test to run.
328        @param retry_for: If the to-be-created job is a retry for an
329                          old job, the afe_job_id of the old job will
330                          be passed in as |retry_for|, which will be
331                          recorded in the new job's keyvals.
332        @returns: A frontend.Job object with an added test_name member.
333                  test_name is used to preserve the higher level TEST_NAME
334                  name of the job.
335        """
336        test_obj = self._afe.create_job(
337            control_file=test.text,
338            name=tools.create_job_name(
339                    self._test_source_build or self.cros_build,
340                    self._tag,
341                    test.name),
342            control_type=test.test_type.capitalize(),
343            meta_hosts=[self._board]*test.sync_count,
344            dependencies=self._create_job_deps(test),
345            keyvals=self._create_keyvals_for_test_job(test, retry_for),
346            max_runtime_mins=self._max_runtime_mins,
347            timeout_mins=self._timeout_mins,
348            parent_job_id=self._suite_job_id,
349            test_retry=test.retries,
350            priority=self._priority,
351            synch_count=test.sync_count,
352            require_ssp=test.require_ssp)
353
354        test_obj.test_name = test.name
355        return test_obj
356
357
358    def _create_job_deps(self, test):
359        """Create job deps list for a test job.
360
361        @returns: A list of dependency strings.
362        """
363        if self._ignore_deps:
364            job_deps = []
365        else:
366            job_deps = list(test.dependencies)
367        job_deps.extend(self._extra_deps)
368        return job_deps
369
370
371    def _create_keyvals_for_test_job(self, test, retry_for=None):
372        """Create keyvals dict for creating a test job.
373
374        @param test: ControlData object for a test to run.
375        @param retry_for: If the to-be-created job is a retry for an
376                          old job, the afe_job_id of the old job will
377                          be passed in as |retry_for|, which will be
378                          recorded in the new job's keyvals.
379        @returns: A keyvals dict for creating the test job.
380        """
381        keyvals = {
382            constants.JOB_BUILD_KEY: self.cros_build,
383            constants.JOB_SUITE_KEY: self._tag,
384            constants.JOB_EXPERIMENTAL_KEY: test.experimental,
385            constants.JOB_BUILDS_KEY: self._builds
386        }
387        # test_source_build is saved to job_keyvals so scheduler can retrieve
388        # the build name from database when compiling autoserv commandline.
389        # This avoid a database change to add a new field in afe_jobs.
390        #
391        # Only add `test_source_build` to job keyvals if the build is different
392        # from the CrOS build or the job uses more than one build, e.g., both
393        # firmware and CrOS will be updated in the dut.
394        # This is for backwards compatibility, so the update Autotest code can
395        # compile an autoserv command line to run in a SSP container using
396        # previous builds.
397        if (self._test_source_build and
398            (self.cros_build != self._test_source_build or
399             len(self._builds) > 1)):
400            keyvals[constants.JOB_TEST_SOURCE_BUILD_KEY] = \
401                    self._test_source_build
402            for prefix, build in self._builds.iteritems():
403                if prefix == provision.FW_RW_VERSION_PREFIX:
404                    keyvals[constants.FWRW_BUILD]= build
405                elif prefix == provision.FW_RO_VERSION_PREFIX:
406                    keyvals[constants.FWRO_BUILD] = build
407        # Add suite job id to keyvals so tko parser can read it from keyval
408        # file.
409        if self._suite_job_id:
410            keyvals[constants.PARENT_JOB_ID] = self._suite_job_id
411        # We drop the old job's id in the new job's keyval file so that
412        # later our tko parser can figure out the retry relationship and
413        # invalidate the results of the old job in tko database.
414        if retry_for:
415            keyvals[constants.RETRY_ORIGINAL_JOB_ID] = retry_for
416        if self._offload_failures_only:
417            keyvals[constants.JOB_OFFLOAD_FAILURES_KEY] = True
418        if self._job_keyvals:
419            for key in constants.INHERITED_KEYVALS:
420                if key in self._job_keyvals:
421                    keyvals[key] = self._job_keyvals[key]
422        return keyvals
423
424
425def _get_cf_retriever(cf_getter, forgiving_parser=True, run_prod_code=False,
426                      test_args=None):
427    """Return the correct _ControlFileRetriever instance.
428
429    If cf_getter is a File system ControlFileGetter, return a
430    _ControlFileRetriever.  This performs a full parse of the root
431    directory associated with the getter. This is the case when it's
432    invoked from suite_preprocessor.
433
434    If cf_getter is a devserver getter, return a
435    _BatchControlFileRetriever.  This looks up the suite_name in a suite
436    to control file map generated at build time, and parses the relevant
437    control files alone. This lookup happens on the devserver, so as far
438    as this method is concerned, both cases are equivalent. If
439    enable_controls_in_batch is switched on, this function will call
440    cf_getter.get_suite_info() to get a dict of control files and
441    contents in batch.
442    """
443    if _should_batch_with(cf_getter):
444        cls = _BatchControlFileRetriever
445    else:
446        cls = _ControlFileRetriever
447    return cls(cf_getter, forgiving_parser, run_prod_code, test_args)
448
449
450def _should_batch_with(cf_getter):
451    """Return whether control files should be fetched in batch.
452
453    This depends on the control file getter and configuration options.
454
455    @param cf_getter: a control_file_getter.ControlFileGetter used to list
456           and fetch the content of control files
457    """
458    return (ENABLE_CONTROLS_IN_BATCH
459            and isinstance(cf_getter, control_file_getter.DevServerGetter))
460
461
462class _ControlFileRetriever(object):
463    """Retrieves control files.
464
465    This returns control data instances, unlike control file getters
466    which simply return the control file text contents.
467    """
468
469    def __init__(self, cf_getter, forgiving_parser=True, run_prod_code=False,
470                 test_args=None):
471        """Initialize instance.
472
473        @param cf_getter: a control_file_getter.ControlFileGetter used to list
474               and fetch the content of control files
475        @param forgiving_parser: If False, will raise ControlVariableExceptions
476                                 if any are encountered when parsing control
477                                 files. Note that this can raise an exception
478                                 for syntax errors in unrelated files, because
479                                 we parse them before applying the predicate.
480        @param run_prod_code: If true, the retrieved tests will run the test
481                              code that lives in prod aka the test code
482                              currently on the lab servers by disabling
483                              SSP for the discovered tests.
484        @param test_args: A dict of args to be seeded in test control file under
485                          the name |args_dict|.
486        """
487        self._cf_getter = cf_getter
488        self._forgiving_parser = forgiving_parser
489        self._run_prod_code = run_prod_code
490        self._test_args = test_args
491
492
493    def retrieve(self, test_name):
494        """Retrieve a test's control data.
495
496        This ignores forgiving_parser because we cannot return a
497        forgiving value.
498
499        @param test_name: Name of test to retrieve.
500
501        @raises ControlVariableException: There is a syntax error in a
502                                          control file.
503
504        @returns a ControlData object
505        """
506        path = self._cf_getter.get_control_file_path(test_name)
507        text = self._cf_getter.get_control_file_contents(path)
508        return self._parse_cf_text(path, text)
509
510
511    def retrieve_for_suite(self, suite_name=''):
512        """Scan through all tests and find all tests.
513
514        @param suite_name: If specified, this method will attempt to restrain
515                           the search space to just this suite's control files.
516
517        @raises ControlVariableException: If forgiving_parser is False and there
518                                          is a syntax error in a control file.
519
520        @returns a dictionary of ControlData objects that based on given
521                 parameters.
522        """
523        control_file_texts = self._get_cf_texts_for_suite(suite_name)
524        return self._parse_cf_text_many(control_file_texts)
525
526
527    def _filter_cf_paths(self, paths):
528        """Remove certain control file paths
529
530        @param paths: Iterable of paths
531        @returns: generator yielding paths
532        """
533        matcher = re.compile(r'[^/]+/(deps|profilers)/.+')
534        return (path for path in paths if not matcher.match(path))
535
536
537    def _get_cf_texts_for_suite(self, suite_name):
538        """Get control file content for given suite.
539
540        @param suite_name: If specified, this method will attempt to restrain
541                           the search space to just this suite's control files.
542        @returns: generator yielding (path, text) tuples
543        """
544        files = self._cf_getter.get_control_file_list(suite_name=suite_name)
545        filtered_files = self._filter_cf_paths(files)
546        for path in filtered_files:
547            yield path, self._cf_getter.get_control_file_contents(path)
548
549
550    def _parse_cf_text_many(self, control_file_texts):
551        """Parse control file texts.
552
553        @param control_file_texts: iterable of (path, text) pairs
554        @returns: a dictionary of ControlData objects
555        """
556        tests = {}
557        for path, text in control_file_texts:
558            # Seed test_args into the control file.
559            if self._test_args:
560                text = tools.inject_vars(self._test_args, text)
561            try:
562                found_test = self._parse_cf_text(path, text)
563            except control_data.ControlVariableException, e:
564                if not self._forgiving_parser:
565                    msg = "Failed parsing %s\n%s" % (path, e)
566                    raise control_data.ControlVariableException(msg)
567                logging.warning("Skipping %s\n%s", path, e)
568            except Exception, e:
569                logging.error("Bad %s\n%s", path, e)
570            else:
571                tests[path] = found_test
572        return tests
573
574
575    def _parse_cf_text(self, path, text):
576        """Parse control file text.
577
578        This ignores forgiving_parser because we cannot return a
579        forgiving value.
580
581        @param path: path to control file
582        @param text: control file text contents
583        @returns: a ControlData object
584
585        @raises ControlVariableException: There is a syntax error in a
586                                          control file.
587        """
588        test = control_data.parse_control_string(
589                text, raise_warnings=True, path=path)
590        test.text = text
591        if self._run_prod_code:
592            test.require_ssp = False
593        return test
594
595
596class _BatchControlFileRetriever(_ControlFileRetriever):
597    """Subclass that can retrieve suite control files in batch."""
598
599
600    def _get_cf_texts_for_suite(self, suite_name):
601        """Get control file content for given suite.
602
603        @param suite_name: If specified, this method will attempt to restrain
604                           the search space to just this suite's control files.
605        @returns: generator yielding (path, text) tuples
606        """
607        suite_info = self._cf_getter.get_suite_info(suite_name=suite_name)
608        files = suite_info.keys()
609        filtered_files = self._filter_cf_paths(files)
610        for path in filtered_files:
611            yield path, suite_info[path]
612
613
614def get_test_source_build(builds, **dargs):
615    """Get the build of test code.
616
617    Get the test source build from arguments. If parameter
618    `test_source_build` is set and has a value, return its value. Otherwise
619    returns the ChromeOS build name if it exists. If ChromeOS build is not
620    specified either, raise SuiteArgumentException.
621
622    @param builds: the builds on which we're running this suite. It's a
623                   dictionary of version_prefix:build.
624    @param **dargs: Any other Suite constructor parameters, as described
625                    in Suite.__init__ docstring.
626
627    @return: The build contains the test code.
628    @raise: SuiteArgumentException if both test_source_build and ChromeOS
629            build are not specified.
630
631    """
632    if dargs.get('test_source_build', None):
633        return dargs['test_source_build']
634    test_source_build = builds.get(provision.CROS_VERSION_PREFIX, None)
635    if not test_source_build:
636        raise error.SuiteArgumentException(
637                'test_source_build must be specified if CrOS build is not '
638                'specified.')
639    return test_source_build
640
641
642def list_all_suites(build, devserver, cf_getter=None):
643    """
644    Parses all ControlData objects with a SUITE tag and extracts all
645    defined suite names.
646
647    @param build: the build on which we're running this suite.
648    @param devserver: the devserver which contains the build.
649    @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
650                      using DevServerGetter.
651
652    @return list of suites
653    """
654    if cf_getter is None:
655        cf_getter = _create_ds_getter(build, devserver)
656
657    suites = set()
658    predicate = lambda t: True
659    for test in find_and_parse_tests(cf_getter, predicate):
660        suites.update(test.suite_tag_parts)
661    return list(suites)
662
663
664def test_file_similarity_predicate(test_file_pattern):
665    """Returns predicate that gets the similarity based on a test's file
666    name pattern.
667
668    Builds a predicate that takes in a parsed control file (a ControlData)
669    and returns a tuple of (file path, ratio), where ratio is the
670    similarity between the test file name and the given test_file_pattern.
671
672    @param test_file_pattern: regular expression (string) to match against
673                              control file names.
674    @return a callable that takes a ControlData and and returns a tuple of
675            (file path, ratio), where ratio is the similarity between the
676            test file name and the given test_file_pattern.
677    """
678    return lambda t: ((None, 0) if not hasattr(t, 'path') else
679            (t.path, difflib.SequenceMatcher(a=t.path,
680                                             b=test_file_pattern).ratio()))
681
682
683def test_name_similarity_predicate(test_name):
684    """Returns predicate that matched based on a test's name.
685
686    Builds a predicate that takes in a parsed control file (a ControlData)
687    and returns a tuple of (test name, ratio), where ratio is the similarity
688    between the test name and the given test_name.
689
690    @param test_name: the test name to base the predicate on.
691    @return a callable that takes a ControlData and returns a tuple of
692            (test name, ratio), where ratio is the similarity between the
693            test name and the given test_name.
694    """
695    return lambda t: ((None, 0) if not hasattr(t, 'name') else
696            (t.name,
697             difflib.SequenceMatcher(a=t.name, b=test_name).ratio()))
698
699
700def matches_attribute_expression_predicate(test_attr_boolstr):
701    """Returns predicate that matches based on boolean expression of
702    attributes.
703
704    Builds a predicate that takes in a parsed control file (a ControlData)
705    ans returns True if the test attributes satisfy the given attribute
706    boolean expression.
707
708    @param test_attr_boolstr: boolean expression of the attributes to be
709                              test, like 'system:all and interval:daily'.
710
711    @return a callable that takes a ControlData and returns True if the test
712            attributes satisfy the given boolean expression.
713    """
714    return lambda t: boolparse_lib.BoolstrResult(
715        test_attr_boolstr, t.attributes)
716
717
718def test_file_matches_pattern_predicate(test_file_pattern):
719    """Returns predicate that matches based on a test's file name pattern.
720
721    Builds a predicate that takes in a parsed control file (a ControlData)
722    and returns True if the test's control file name matches the given
723    regular expression.
724
725    @param test_file_pattern: regular expression (string) to match against
726                              control file names.
727    @return a callable that takes a ControlData and and returns
728            True if control file name matches the pattern.
729    """
730    return lambda t: hasattr(t, 'path') and re.match(test_file_pattern,
731                                                     t.path)
732
733
734def test_name_matches_pattern_predicate(test_name_pattern):
735    """Returns predicate that matches based on a test's name pattern.
736
737    Builds a predicate that takes in a parsed control file (a ControlData)
738    and returns True if the test name matches the given regular expression.
739
740    @param test_name_pattern: regular expression (string) to match against
741                              test names.
742    @return a callable that takes a ControlData and returns
743            True if the name fields matches the pattern.
744    """
745    return lambda t: hasattr(t, 'name') and re.match(test_name_pattern,
746                                                     t.name)
747
748
749def test_name_equals_predicate(test_name):
750    """Returns predicate that matched based on a test's name.
751
752    Builds a predicate that takes in a parsed control file (a ControlData)
753    and returns True if the test name is equal to |test_name|.
754
755    @param test_name: the test name to base the predicate on.
756    @return a callable that takes a ControlData and looks for |test_name|
757            in that ControlData's name.
758    """
759    return lambda t: hasattr(t, 'name') and test_name == t.name
760
761
762def name_in_tag_similarity_predicate(name):
763    """Returns predicate that takes a control file and gets the similarity
764    of the suites in the control file and the given name.
765
766    Builds a predicate that takes in a parsed control file (a ControlData)
767    and returns a list of tuples of (suite name, ratio), where suite name
768    is each suite listed in the control file, and ratio is the similarity
769    between each suite and the given name.
770
771    @param name: the suite name to base the predicate on.
772    @return a callable that takes a ControlData and returns a list of tuples
773            of (suite name, ratio), where suite name is each suite listed in
774            the control file, and ratio is the similarity between each suite
775            and the given name.
776    """
777    return lambda t: [(suite,
778                       difflib.SequenceMatcher(a=suite, b=name).ratio())
779                      for suite in t.suite_tag_parts] or [(None, 0)]
780
781
782def name_in_tag_predicate(name):
783    """Returns predicate that takes a control file and looks for |name|.
784
785    Builds a predicate that takes in a parsed control file (a ControlData)
786    and returns True if the SUITE tag is present and contains |name|.
787
788    @param name: the suite name to base the predicate on.
789    @return a callable that takes a ControlData and looks for |name| in that
790            ControlData object's suite member.
791    """
792    return lambda t: name in t.suite_tag_parts
793
794
795def create_fs_getter(autotest_dir):
796    """
797    @param autotest_dir: the place to find autotests.
798    @return a FileSystemGetter instance that looks under |autotest_dir|.
799    """
800    # currently hard-coded places to look for tests.
801    subpaths = ['server/site_tests', 'client/site_tests',
802                'server/tests', 'client/tests']
803    directories = [os.path.join(autotest_dir, p) for p in subpaths]
804    return control_file_getter.FileSystemGetter(directories)
805
806
807def _create_ds_getter(build, devserver):
808    """
809    @param build: the build on which we're running this suite.
810    @param devserver: the devserver which contains the build.
811    @return a FileSystemGetter instance that looks under |autotest_dir|.
812    """
813    return control_file_getter.DevServerGetter(build, devserver)
814
815
816def _non_experimental_tests_predicate(test_data):
817    """Test predicate for non-experimental tests."""
818    return not test_data.experimental
819
820
821def find_and_parse_tests(cf_getter, predicate, suite_name='',
822                         add_experimental=False, forgiving_parser=True,
823                         run_prod_code=False, test_args=None):
824    """
825    Function to scan through all tests and find eligible tests.
826
827    Search through all tests based on given cf_getter, suite_name,
828    add_experimental and forgiving_parser, return the tests that match
829    given predicate.
830
831    @param cf_getter: a control_file_getter.ControlFileGetter used to list
832           and fetch the content of control files
833    @param predicate: a function that should return True when run over a
834           ControlData representation of a control file that should be in
835           this Suite.
836    @param suite_name: If specified, this method will attempt to restrain
837                       the search space to just this suite's control files.
838    @param add_experimental: add tests with experimental attribute set.
839    @param forgiving_parser: If False, will raise ControlVariableExceptions
840                             if any are encountered when parsing control
841                             files. Note that this can raise an exception
842                             for syntax errors in unrelated files, because
843                             we parse them before applying the predicate.
844    @param run_prod_code: If true, the suite will run the test code that
845                          lives in prod aka the test code currently on the
846                          lab servers by disabling SSP for the discovered
847                          tests.
848    @param test_args: A dict of args to be seeded in test control file.
849
850    @raises ControlVariableException: If forgiving_parser is False and there
851                                      is a syntax error in a control file.
852
853    @return list of ControlData objects that should be run, with control
854            file text added in |text| attribute. Results are sorted based
855            on the TIME setting in control file, slowest test comes first.
856    """
857    logging.debug('Getting control file list for suite: %s', suite_name)
858    retriever = _get_cf_retriever(cf_getter,
859                                  forgiving_parser=forgiving_parser,
860                                  run_prod_code=run_prod_code,
861                                  test_args=test_args)
862    tests = retriever.retrieve_for_suite(suite_name)
863    logging.debug('Parsed %s control files.', len(tests))
864    if not add_experimental:
865        predicate = _ComposedPredicate([predicate,
866                                        _non_experimental_tests_predicate])
867    tests = [test for test in tests.itervalues() if predicate(test)]
868    tests.sort(key=lambda t:
869               control_data.ControlData.get_test_time_index(t.time),
870               reverse=True)
871    return tests
872
873
874def find_possible_tests(cf_getter, predicate, suite_name='', count=10):
875    """
876    Function to scan through all tests and find possible tests.
877
878    Search through all tests based on given cf_getter, suite_name,
879    add_experimental and forgiving_parser. Use the given predicate to
880    calculate the similarity and return the top 10 matches.
881
882    @param cf_getter: a control_file_getter.ControlFileGetter used to list
883           and fetch the content of control files
884    @param predicate: a function that should return a tuple of (name, ratio)
885           when run over a ControlData representation of a control file that
886           should be in this Suite. `name` is the key to be compared, e.g.,
887           a suite name or test name. `ratio` is a value between [0,1]
888           indicating the similarity of `name` and the value to be compared.
889    @param suite_name: If specified, this method will attempt to restrain
890                       the search space to just this suite's control files.
891    @param count: Number of suggestions to return, default to 10.
892
893    @return list of top names that similar to the given test, sorted by
894            match ratio.
895    """
896    logging.debug('Getting control file list for suite: %s', suite_name)
897    tests = _get_cf_retriever(cf_getter).retrieve_for_suite(suite_name)
898    logging.debug('Parsed %s control files.', len(tests))
899    similarities = {}
900    for test in tests.itervalues():
901        ratios = predicate(test)
902        # Some predicates may return a list of tuples, e.g.,
903        # name_in_tag_similarity_predicate. Convert all returns to a list.
904        if not isinstance(ratios, list):
905            ratios = [ratios]
906        for name, ratio in ratios:
907            similarities[name] = ratio
908    return [s[0] for s in
909            sorted(similarities.items(), key=operator.itemgetter(1),
910                   reverse=True)][:count]
911
912
913def _deprecated_suite_method(func):
914    """Decorator for deprecated Suite static methods.
915
916    TODO(ayatane): This is used to decorate functions that are called as
917    static methods on Suite.
918    """
919    @functools.wraps(func)
920    def wrapper(*args, **kwargs):
921        """Wraps |func| for warning."""
922        warnings.warn('Calling method "%s" from Suite is deprecated' %
923                      func.__name__)
924        return func(*args, **kwargs)
925    return staticmethod(wrapper)
926
927
928class _BaseSuite(object):
929    """
930    A suite of tests, defined by some predicate over control file variables.
931
932    Given a place to search for control files a predicate to match the desired
933    tests, can gather tests and fire off jobs to run them, and then wait for
934    results.
935
936    @var _predicate: a function that should return True when run over a
937         ControlData representation of a control file that should be in
938         this Suite.
939    @var _tag: a string with which to tag jobs run in this suite.
940    @var _builds: the builds on which we're running this suite.
941    @var _afe: an instance of AFE as defined in server/frontend.py.
942    @var _tko: an instance of TKO as defined in server/frontend.py.
943    @var _jobs: currently scheduled jobs, if any.
944    @var _jobs_to_tests: a dictionary that maps job ids to tests represented
945                         ControlData objects.
946    @var _retry: a bool value indicating whether jobs should be retried on
947                 failure.
948    @var _retry_handler: a RetryHandler object.
949
950    """
951
952
953    def __init__(
954            self,
955            tests,
956            tag,
957            builds,
958            board,
959            afe=None,
960            tko=None,
961            pool=None,
962            results_dir=None,
963            max_runtime_mins=24*60,
964            timeout_mins=24*60,
965            file_bugs=False,
966            suite_job_id=None,
967            ignore_deps=False,
968            extra_deps=None,
969            priority=priorities.Priority.DEFAULT,
970            wait_for_results=True,
971            job_retry=False,
972            max_retries=sys.maxint,
973            offload_failures_only=False,
974            test_source_build=None,
975            job_keyvals=None
976    ):
977        """Initialize instance.
978
979        @param tests: Iterable of tests to run.
980        @param tag: a string with which to tag jobs run in this suite.
981        @param builds: the builds on which we're running this suite.
982        @param board: the board on which we're running this suite.
983        @param afe: an instance of AFE as defined in server/frontend.py.
984        @param tko: an instance of TKO as defined in server/frontend.py.
985        @param pool: Specify the pool of machines to use for scheduling
986                purposes.
987        @param results_dir: The directory where the job can write results to.
988                            This must be set if you want job_id of sub-jobs
989                            list in the job keyvals.
990        @param max_runtime_mins: Maximum suite runtime, in minutes.
991        @param timeout: Maximum job lifetime, in hours.
992        @param suite_job_id: Job id that will act as parent id to all sub jobs.
993                             Default: None
994        @param ignore_deps: True if jobs should ignore the DEPENDENCIES
995                            attribute and skip applying of dependency labels.
996                            (Default:False)
997        @param extra_deps: A list of strings which are the extra DEPENDENCIES
998                           to add to each test being scheduled.
999        @param priority: Integer priority level.  Higher is more important.
1000        @param wait_for_results: Set to False to run the suite job without
1001                                 waiting for test jobs to finish. Default is
1002                                 True.
1003        @param job_retry: A bool value indicating whether jobs should be retired
1004                          on failure. If True, the field 'JOB_RETRIES' in
1005                          control files will be respected. If False, do not
1006                          retry.
1007        @param max_retries: Maximum retry limit at suite level.
1008                            Regardless how many times each individual test
1009                            has been retried, the total number of retries
1010                            happening in the suite can't exceed _max_retries.
1011                            Default to sys.maxint.
1012        @param offload_failures_only: Only enable gs_offloading for failed
1013                                      jobs.
1014        @param test_source_build: Build that contains the server-side test code.
1015        @param job_keyvals: General job keyvals to be inserted into keyval file,
1016                            which will be used by tko/parse later.
1017        """
1018
1019        self.tests = list(tests)
1020        self._tag = tag
1021        self._builds = builds
1022        self._results_dir = results_dir
1023        self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
1024                                                         delay_sec=10,
1025                                                         debug=False)
1026        self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30,
1027                                                         delay_sec=10,
1028                                                         debug=False)
1029        self._jobs = []
1030        self._jobs_to_tests = {}
1031
1032        self._file_bugs = file_bugs
1033        self._suite_job_id = suite_job_id
1034        self._job_retry=job_retry
1035        self._max_retries = max_retries
1036        # RetryHandler to be initialized in schedule()
1037        self._retry_handler = None
1038        self.wait_for_results = wait_for_results
1039        self._job_keyvals = job_keyvals
1040
1041        if extra_deps is None:
1042            extra_deps = []
1043        extra_deps.append(board)
1044        if pool:
1045            extra_deps.append(pool)
1046        self._job_creator = _SuiteChildJobCreator(
1047            tag=tag,
1048            builds=builds,
1049            board=board,
1050            afe=afe,
1051            max_runtime_mins=max_runtime_mins,
1052            timeout_mins=timeout_mins,
1053            suite_job_id=suite_job_id,
1054            ignore_deps=ignore_deps,
1055            extra_deps=extra_deps,
1056            priority=priority,
1057            offload_failures_only=offload_failures_only,
1058            test_source_build=test_source_build,
1059            job_keyvals=job_keyvals,
1060        )
1061
1062
1063    def _schedule_test(self, record, test, retry_for=None):
1064        """Schedule a single test and return the job.
1065
1066        Schedule a single test by creating a job, and then update relevant
1067        data structures that are used to keep track of all running jobs.
1068
1069        Emits a TEST_NA status log entry if it failed to schedule the test due
1070        to NoEligibleHostException or a non-existent board label.
1071
1072        Returns a frontend.Job object if the test is successfully scheduled.
1073        If scheduling failed due to NoEligibleHostException or a non-existent
1074        board label, returns None.
1075
1076        @param record: A callable to use for logging.
1077                       prototype: record(base_job.status_log_entry)
1078        @param test: ControlData for a test to run.
1079        @param retry_for: If we are scheduling a test to retry an
1080                          old job, the afe_job_id of the old job
1081                          will be passed in as |retry_for|.
1082
1083        @returns: A frontend.Job object or None
1084        """
1085        msg = 'Scheduling %s' % test.name
1086        if retry_for:
1087            msg = msg + ', to retry afe job %d' % retry_for
1088        logging.debug(msg)
1089        begin_time_str = datetime.datetime.now().strftime(time_utils.TIME_FMT)
1090        try:
1091            job = self._job_creator.create_job(test, retry_for=retry_for)
1092        except (error.NoEligibleHostException, proxy.ValidationError) as e:
1093            if (isinstance(e, error.NoEligibleHostException)
1094                or (isinstance(e, proxy.ValidationError)
1095                    and _is_nonexistent_board_error(e))):
1096                # Treat a dependency on a non-existent board label the same as
1097                # a dependency on a board that exists, but for which there's no
1098                # hardware.
1099                logging.debug('%s not applicable for this board/pool. '
1100                              'Emitting TEST_NA.', test.name)
1101                Status('TEST_NA', test.name,
1102                       'Skipping:  test not supported on this board/pool.',
1103                       begin_time_str=begin_time_str).record_all(record)
1104                return None
1105            else:
1106                raise e
1107        except (error.RPCException, proxy.JSONRPCException):
1108            if retry_for:
1109                # Mark that we've attempted to retry the old job.
1110                self._retry_handler.set_attempted(job_id=retry_for)
1111            raise
1112        else:
1113            self._jobs.append(job)
1114            self._jobs_to_tests[job.id] = test
1115            if retry_for:
1116                # A retry job was just created, record it.
1117                self._retry_handler.add_retry(
1118                        old_job_id=retry_for, new_job_id=job.id)
1119                retry_count = (test.job_retries -
1120                               self._retry_handler.get_retry_max(job.id))
1121                logging.debug('Job %d created to retry job %d. '
1122                              'Have retried for %d time(s)',
1123                              job.id, retry_for, retry_count)
1124            self._remember_job_keyval(job)
1125            return job
1126
1127
1128    def schedule(self, record):
1129        #pylint: disable-msg=C0111
1130        """
1131        Schedule jobs using |self._afe|.
1132
1133        frontend.Job objects representing each scheduled job will be put in
1134        |self._jobs|.
1135
1136        @param record: A callable to use for logging.
1137                       prototype: record(base_job.status_log_entry)
1138        @returns: The number of tests that were scheduled.
1139        """
1140        scheduled_test_names = []
1141        logging.debug('Discovered %d tests.', len(self.tests))
1142
1143        Status('INFO', 'Start %s' % self._tag).record_result(record)
1144        try:
1145            # Write job_keyvals into keyval file.
1146            if self._job_keyvals:
1147                utils.write_keyval(self._results_dir, self._job_keyvals)
1148
1149            # TODO(crbug.com/730885): This is a hack to protect tests that are
1150            # not usually retried from getting hit by a provision error when run
1151            # as part of a suite. Remove this hack once provision is separated
1152            # out in its own suite.
1153            self._bump_up_test_retries(self.tests)
1154            for test in self.tests:
1155                scheduled_job = self._schedule_test(record, test)
1156                if scheduled_job is not None:
1157                    scheduled_test_names.append(test.name)
1158
1159            # Write the num of scheduled tests and name of them to keyval file.
1160            logging.debug('Scheduled %d tests, writing the total to keyval.',
1161                          len(scheduled_test_names))
1162            utils.write_keyval(
1163                self._results_dir,
1164                self._make_scheduled_tests_keyvals(scheduled_test_names))
1165        except Exception:  # pylint: disable=W0703
1166            logging.exception('Exception while scheduling suite')
1167            Status('FAIL', self._tag,
1168                   'Exception while scheduling suite').record_result(record)
1169
1170        if self._job_retry:
1171            self._retry_handler = RetryHandler(
1172                    initial_jobs_to_tests=self._jobs_to_tests,
1173                    max_retries=self._max_retries)
1174        return len(scheduled_test_names)
1175
1176
1177    def _bump_up_test_retries(self, tests):
1178        """Bump up individual test retries to match suite retry options."""
1179        if not self._job_retry:
1180            return
1181
1182        for test in tests:
1183            if not test.job_retries:
1184                logging.debug(
1185                        'Test %s requested no retries, but suite requires '
1186                        'retries. Bumping retries up to 1. '
1187                        '(See crbug.com/730885)',
1188                        test.name)
1189                test.job_retries = 1
1190
1191
1192    def _make_scheduled_tests_keyvals(self, scheduled_test_names):
1193        """Make a keyvals dict to write for scheduled test names.
1194
1195        @param scheduled_test_names: A list of scheduled test name strings.
1196
1197        @returns: A keyvals dict.
1198        """
1199        return {
1200            constants.SCHEDULED_TEST_COUNT_KEY: len(scheduled_test_names),
1201            constants.SCHEDULED_TEST_NAMES_KEY: repr(scheduled_test_names),
1202        }
1203
1204
1205    def _should_report(self, result):
1206        """
1207        Returns True if this failure requires to be reported.
1208
1209        @param result: A result, encapsulating the status of the failed job.
1210        @return: True if we should report this failure.
1211        """
1212        if self._has_retry(result):
1213            return False
1214
1215        return (self._file_bugs and result.test_executed and
1216                not result.is_testna() and
1217                result.is_worse_than(job_status.Status('GOOD', '', 'reason')))
1218
1219
1220    def _has_retry(self, result):
1221        """
1222        Return True if this result gets to retry.
1223
1224        @param result: A result, encapsulating the status of the failed job.
1225        @return: bool
1226        """
1227        return (self._job_retry
1228                and self._retry_handler.has_following_retry(result))
1229
1230
1231    def wait(self, record, reporter):
1232        """
1233        Polls for the job statuses, using |record| to print status when each
1234        completes.
1235
1236        @param record: callable that records job status.
1237                 prototype:
1238                   record(base_job.status_log_entry)
1239        @param reporter: _ResultReporter instance.
1240        """
1241        try:
1242            if self._suite_job_id:
1243                results_generator = job_status.wait_for_child_results(
1244                        self._afe, self._tko, self._suite_job_id)
1245            else:
1246                logging.warning('Unknown suite_job_id, falling back to less '
1247                                'efficient results_generator.')
1248                results_generator = job_status.wait_for_results(self._afe,
1249                                                                self._tko,
1250                                                                self._jobs)
1251            for result in results_generator:
1252                self._record_result(
1253                    result=result,
1254                    record=record,
1255                    results_generator=results_generator,
1256                    reporter=reporter)
1257
1258        except Exception:  # pylint: disable=W0703
1259            logging.exception('Exception waiting for results')
1260            Status('FAIL', self._tag,
1261                   'Exception waiting for results').record_result(record)
1262
1263
1264    def get_result_reporter(self, bug_template):
1265        """Return the _ResultReporter instance to use for the suite.
1266
1267        @param bug_template: A template dictionary specifying the default bug
1268                             filing options for failures in this suite.
1269        """
1270        # reporting modules have dependency on external packages, e.g., httplib2
1271        # Such dependency can cause issue to any module tries to import suite.py
1272        # without building site-packages first. Since the reporting modules are
1273        # only used in this function, move the imports here avoid the
1274        # requirement of building site packages to use other functions in this
1275        # module.
1276        from autotest_lib.server.cros.dynamic_suite import reporting
1277
1278        if self._should_file_bugs:
1279            if self._file_bugs:
1280                bug_reporter = reporting.Reporter()
1281            else:
1282                bug_reporter = reporting.NullReporter()
1283            return _BugResultReporter(self, bug_reporter, bug_template)
1284        else:
1285            return _EmailResultReporter(self, bug_template)
1286
1287
1288    def _record_result(self, result, record, results_generator, reporter):
1289        """
1290        Record a single test job result.
1291
1292        @param result: Status instance for job.
1293        @param record: callable that records job status.
1294                 prototype:
1295                   record(base_job.status_log_entry)
1296        @param results_generator: Results generator for sending job retries.
1297        @param reporter: _ResultReporter instance.
1298        """
1299        result.record_all(record)
1300        self._remember_job_keyval(result)
1301
1302        if self._job_retry and self._retry_handler._should_retry(result):
1303            test = self._jobs_to_tests[result.id]
1304            try:
1305                new_job = self._schedule_test(
1306                        record=record, test=test, retry_for=result.id)
1307            except (error.RPCException, proxy.JSONRPCException) as e:
1308                logging.error('Failed to schedule test: %s, Reason: %s',
1309                              test.name, e)
1310            else:
1311                results_generator.send([new_job])
1312
1313        # TODO (fdeng): If the suite times out before a retry could
1314        # finish, we would lose the chance to file a bug for the
1315        # original job.
1316        if self._should_report(result):
1317            reporter.report(result)
1318
1319    def _get_bug_template(self, result, bug_template):
1320        """Get BugTemplate for test job.
1321
1322        @param result: Status instance for job.
1323        @param bug_template: A template dictionary specifying the default bug
1324                             filing options for failures in this suite.
1325        @returns: BugTemplate instance
1326        """
1327        # reporting modules have dependency on external packages, e.g., httplib2
1328        # Such dependency can cause issue to any module tries to import suite.py
1329        # without building site-packages first. Since the reporting modules are
1330        # only used in this function, move the imports here avoid the
1331        # requirement of building site packages to use other functions in this
1332        # module.
1333        from autotest_lib.server.cros.dynamic_suite import reporting_utils
1334
1335        # Try to merge with bug template in test control file.
1336        template = reporting_utils.BugTemplate(bug_template)
1337        try:
1338            test_data = self._jobs_to_tests[result.id]
1339            return template.finalize_bug_template(
1340                    test_data.bug_template)
1341        except AttributeError:
1342            # Test control file does not have bug template defined.
1343            return template.bug_template
1344        except reporting_utils.InvalidBugTemplateException as e:
1345            logging.error('Merging bug templates failed with '
1346                          'error: %s An empty bug template will '
1347                          'be used.', e)
1348            return {}
1349
1350
1351    def _get_test_bug(self, result):
1352        """Get TestBug for the given result.
1353
1354        @param result: Status instance for a test job.
1355        @returns: TestBug instance.
1356        """
1357        # reporting modules have dependency on external packages, e.g., httplib2
1358        # Such dependency can cause issue to any module tries to import suite.py
1359        # without building site-packages first. Since the reporting modules are
1360        # only used in this function, move the imports here avoid the
1361        # requirement of building site packages to use other functions in this
1362        # module.
1363        from autotest_lib.server.cros.dynamic_suite import reporting
1364
1365        job_views = self._tko.run('get_detailed_test_views',
1366                                  afe_job_id=result.id)
1367        return reporting.TestBug(self._job_creator.cros_build,
1368                utils.get_chrome_version(job_views),
1369                self._tag,
1370                result)
1371
1372
1373    @property
1374    def _should_file_bugs(self):
1375        """Return whether bugs should be filed.
1376
1377        @returns: bool
1378        """
1379        # File bug when failure is one of the _FILE_BUG_SUITES,
1380        # otherwise send an email to the owner anc cc.
1381        return self._tag in _FILE_BUG_SUITES
1382
1383
1384    def _file_bug(self, result, bug_reporter, bug_template):
1385        """File a bug for a test job result.
1386
1387        @param result: Status instance for job.
1388        @param bug_reporter: Reporter instance for reporting bugs.
1389        @param bug_template: A template dictionary specifying the default bug
1390                             filing options for failures in this suite.
1391        """
1392        bug_id, bug_count = bug_reporter.report(
1393                self._get_test_bug(result),
1394                self._get_bug_template(result, bug_template))
1395
1396        # We use keyvals to communicate bugs filed with run_suite.
1397        if bug_id is not None:
1398            bug_keyvals = tools.create_bug_keyvals(
1399                    result.id, result.test_name,
1400                    (bug_id, bug_count))
1401            try:
1402                utils.write_keyval(self._results_dir,
1403                                   bug_keyvals)
1404            except ValueError:
1405                logging.error('Unable to log bug keyval for:%s',
1406                              result.test_name)
1407
1408
1409    def abort(self):
1410        """
1411        Abort all scheduled test jobs.
1412        """
1413        if self._jobs:
1414            job_ids = [job.id for job in self._jobs]
1415            self._afe.run('abort_host_queue_entries', job__id__in=job_ids)
1416
1417
1418    def _remember_job_keyval(self, job):
1419        """
1420        Record provided job as a suite job keyval, for later referencing.
1421
1422        @param job: some representation of a job that has the attributes:
1423                    id, test_name, and owner
1424        """
1425        if self._results_dir and job.id and job.owner and job.test_name:
1426            job_id_owner = '%s-%s' % (job.id, job.owner)
1427            logging.debug('Adding job keyval for %s=%s',
1428                          job.test_name, job_id_owner)
1429            utils.write_keyval(
1430                self._results_dir,
1431                {hashlib.md5(job.test_name).hexdigest(): job_id_owner})
1432
1433
1434class Suite(_BaseSuite):
1435    """
1436    A suite of tests, defined by some predicate over control file variables.
1437
1438    Given a place to search for control files a predicate to match the desired
1439    tests, can gather tests and fire off jobs to run them, and then wait for
1440    results.
1441
1442    @var _predicate: a function that should return True when run over a
1443         ControlData representation of a control file that should be in
1444         this Suite.
1445    @var _tag: a string with which to tag jobs run in this suite.
1446    @var _builds: the builds on which we're running this suite.
1447    @var _afe: an instance of AFE as defined in server/frontend.py.
1448    @var _tko: an instance of TKO as defined in server/frontend.py.
1449    @var _jobs: currently scheduled jobs, if any.
1450    @var _jobs_to_tests: a dictionary that maps job ids to tests represented
1451                         ControlData objects.
1452    @var _cf_getter: a control_file_getter.ControlFileGetter
1453    @var _retry: a bool value indicating whether jobs should be retried on
1454                 failure.
1455    @var _retry_handler: a RetryHandler object.
1456
1457    """
1458
1459    # TODO(ayatane): These methods are kept on the Suite class for
1460    # backward compatibility.
1461    find_and_parse_tests = _deprecated_suite_method(find_and_parse_tests)
1462    find_possible_tests = _deprecated_suite_method(find_possible_tests)
1463    create_fs_getter = _deprecated_suite_method(create_fs_getter)
1464    name_in_tag_predicate = _deprecated_suite_method(name_in_tag_predicate)
1465    name_in_tag_similarity_predicate = _deprecated_suite_method(
1466            name_in_tag_similarity_predicate)
1467    test_name_equals_predicate = _deprecated_suite_method(
1468            test_name_equals_predicate)
1469    test_name_matches_pattern_predicate = _deprecated_suite_method(
1470            test_name_matches_pattern_predicate)
1471    test_file_matches_pattern_predicate = _deprecated_suite_method(
1472            test_file_matches_pattern_predicate)
1473    matches_attribute_expression_predicate = _deprecated_suite_method(
1474            matches_attribute_expression_predicate)
1475    test_name_similarity_predicate = _deprecated_suite_method(
1476            test_name_similarity_predicate)
1477    test_file_similarity_predicate = _deprecated_suite_method(
1478            test_file_similarity_predicate)
1479    list_all_suites = _deprecated_suite_method(list_all_suites)
1480    get_test_source_build = _deprecated_suite_method(get_test_source_build)
1481
1482
1483    @classmethod
1484    def create_from_predicates(cls, predicates, builds, board, devserver,
1485                               cf_getter=None, name='ad_hoc_suite',
1486                               run_prod_code=False, **dargs):
1487        """
1488        Create a Suite using a given predicate test filters.
1489
1490        Uses supplied predicate(s) to instantiate a Suite. Looks for tests in
1491        |autotest_dir| and will schedule them using |afe|.  Pulls control files
1492        from the default dev server. Results will be pulled from |tko| upon
1493        completion.
1494
1495        @param predicates: A list of callables that accept ControlData
1496                           representations of control files. A test will be
1497                           included in suite if all callables in this list
1498                           return True on the given control file.
1499        @param builds: the builds on which we're running this suite. It's a
1500                       dictionary of version_prefix:build.
1501        @param board: the board on which we're running this suite.
1502        @param devserver: the devserver which contains the build.
1503        @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
1504                          using DevServerGetter.
1505        @param name: name of suite. Defaults to 'ad_hoc_suite'
1506        @param run_prod_code: If true, the suite will run the tests that
1507                              lives in prod aka the test code currently on the
1508                              lab servers.
1509        @param **dargs: Any other Suite constructor parameters, as described
1510                        in Suite.__init__ docstring.
1511        @return a Suite instance.
1512        """
1513        if cf_getter is None:
1514            if run_prod_code:
1515                cf_getter = create_fs_getter(_AUTOTEST_DIR)
1516            else:
1517                build = get_test_source_build(builds, **dargs)
1518                cf_getter = _create_ds_getter(build, devserver)
1519
1520        return cls(predicates,
1521                   name, builds, board, cf_getter, run_prod_code, **dargs)
1522
1523
1524    @classmethod
1525    def create_from_name(cls, name, builds, board, devserver, cf_getter=None,
1526                         **dargs):
1527        """
1528        Create a Suite using a predicate based on the SUITE control file var.
1529
1530        Makes a predicate based on |name| and uses it to instantiate a Suite
1531        that looks for tests in |autotest_dir| and will schedule them using
1532        |afe|.  Pulls control files from the default dev server.
1533        Results will be pulled from |tko| upon completion.
1534
1535        @param name: a value of the SUITE control file variable to search for.
1536        @param builds: the builds on which we're running this suite. It's a
1537                       dictionary of version_prefix:build.
1538        @param board: the board on which we're running this suite.
1539        @param devserver: the devserver which contains the build.
1540        @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
1541                          using DevServerGetter.
1542        @param **dargs: Any other Suite constructor parameters, as described
1543                        in Suite.__init__ docstring.
1544        @return a Suite instance.
1545        """
1546        if cf_getter is None:
1547            build = get_test_source_build(builds, **dargs)
1548            cf_getter = _create_ds_getter(build, devserver)
1549
1550        return cls([name_in_tag_predicate(name)],
1551                   name, builds, board, cf_getter, **dargs)
1552
1553
1554    def __init__(
1555            self,
1556            predicates,
1557            tag,
1558            builds,
1559            board,
1560            cf_getter,
1561            run_prod_code=False,
1562            afe=None,
1563            tko=None,
1564            pool=None,
1565            results_dir=None,
1566            max_runtime_mins=24*60,
1567            timeout_mins=24*60,
1568            file_bugs=False,
1569            suite_job_id=None,
1570            ignore_deps=False,
1571            extra_deps=None,
1572            priority=priorities.Priority.DEFAULT,
1573            forgiving_parser=True,
1574            wait_for_results=True,
1575            job_retry=False,
1576            max_retries=sys.maxint,
1577            offload_failures_only=False,
1578            test_source_build=None,
1579            job_keyvals=None,
1580            test_args=None
1581    ):
1582        """
1583        Constructor
1584
1585        @param predicates: A list of callables that accept ControlData
1586                           representations of control files. A test will be
1587                           included in suite if all callables in this list
1588                           return True on the given control file.
1589        @param tag: a string with which to tag jobs run in this suite.
1590        @param builds: the builds on which we're running this suite.
1591        @param board: the board on which we're running this suite.
1592        @param cf_getter: a control_file_getter.ControlFileGetter
1593        @param afe: an instance of AFE as defined in server/frontend.py.
1594        @param tko: an instance of TKO as defined in server/frontend.py.
1595        @param pool: Specify the pool of machines to use for scheduling
1596                purposes.
1597        @param run_prod_code: If true, the suite will run the test code that
1598                              lives in prod aka the test code currently on the
1599                              lab servers.
1600        @param results_dir: The directory where the job can write results to.
1601                            This must be set if you want job_id of sub-jobs
1602                            list in the job keyvals.
1603        @param max_runtime_mins: Maximum suite runtime, in minutes.
1604        @param timeout: Maximum job lifetime, in hours.
1605        @param suite_job_id: Job id that will act as parent id to all sub jobs.
1606                             Default: None
1607        @param ignore_deps: True if jobs should ignore the DEPENDENCIES
1608                            attribute and skip applying of dependency labels.
1609                            (Default:False)
1610        @param extra_deps: A list of strings which are the extra DEPENDENCIES
1611                           to add to each test being scheduled.
1612        @param priority: Integer priority level.  Higher is more important.
1613        @param wait_for_results: Set to False to run the suite job without
1614                                 waiting for test jobs to finish. Default is
1615                                 True.
1616        @param job_retry: A bool value indicating whether jobs should be retired
1617                          on failure. If True, the field 'JOB_RETRIES' in
1618                          control files will be respected. If False, do not
1619                          retry.
1620        @param max_retries: Maximum retry limit at suite level.
1621                            Regardless how many times each individual test
1622                            has been retried, the total number of retries
1623                            happening in the suite can't exceed _max_retries.
1624                            Default to sys.maxint.
1625        @param offload_failures_only: Only enable gs_offloading for failed
1626                                      jobs.
1627        @param test_source_build: Build that contains the server-side test code.
1628        @param job_keyvals: General job keyvals to be inserted into keyval file,
1629                            which will be used by tko/parse later.
1630        @param test_args: A dict of args passed all the way to each individual
1631                          test that will be actually ran.
1632
1633        """
1634        tests = find_and_parse_tests(
1635                cf_getter,
1636                _ComposedPredicate(predicates),
1637                tag,
1638                forgiving_parser=forgiving_parser,
1639                run_prod_code=run_prod_code,
1640                test_args=test_args,
1641        )
1642        super(Suite, self).__init__(
1643                tests=tests,
1644                tag=tag,
1645                builds=builds,
1646                board=board,
1647                afe=afe,
1648                tko=tko,
1649                pool=pool,
1650                results_dir=results_dir,
1651                max_runtime_mins=max_runtime_mins,
1652                timeout_mins=timeout_mins,
1653                file_bugs=file_bugs,
1654                suite_job_id=suite_job_id,
1655                ignore_deps=ignore_deps,
1656                extra_deps=extra_deps,
1657                priority=priority,
1658                wait_for_results=wait_for_results,
1659                job_retry=job_retry,
1660                max_retries=max_retries,
1661                offload_failures_only=offload_failures_only,
1662                test_source_build=test_source_build,
1663                job_keyvals=job_keyvals)
1664
1665
1666class ProvisionSuite(_BaseSuite):
1667    """
1668    A suite for provisioning DUTs.
1669
1670    This is done by creating dummy_Pass tests.
1671    """
1672
1673
1674    def __init__(
1675            self,
1676            tag,
1677            builds,
1678            board,
1679            count,
1680            devserver,
1681            cf_getter=None,
1682            run_prod_code=False,
1683            test_args=None,
1684            test_source_build=None,
1685            **kwargs):
1686        """
1687        Constructor
1688
1689        @param tag: a string with which to tag jobs run in this suite.
1690        @param builds: the builds on which we're running this suite.
1691        @param board: the board on which we're running this suite.
1692        @param count: number of dummy tests to make
1693        @param devserver: the devserver which contains the build.
1694        @param cf_getter: a control_file_getter.ControlFileGetter.
1695        @param test_args: A dict of args passed all the way to each individual
1696                          test that will be actually ran.
1697        @param test_source_build: Build that contains the server-side test code.
1698        @param kwargs: Various keyword arguments passed to
1699                       _BaseSuite constructor.
1700        """
1701        dummy_test = _load_dummy_test(
1702                builds, devserver, cf_getter,
1703                run_prod_code, test_args, test_source_build)
1704
1705        super(ProvisionSuite, self).__init__(
1706                tests=[dummy_test] * count,
1707                tag=tag,
1708                builds=builds,
1709                board=board,
1710                **kwargs)
1711
1712
1713def _load_dummy_test(
1714        builds,
1715        devserver,
1716        cf_getter=None,
1717        run_prod_code=False,
1718        test_args=None,
1719        test_source_build=None):
1720    """
1721    Load and return the dummy pass test.
1722
1723    @param builds: the builds on which we're running this suite.
1724    @param devserver: the devserver which contains the build.
1725    @param cf_getter: a control_file_getter.ControlFileGetter.
1726    @param test_args: A dict of args passed all the way to each individual
1727                      test that will be actually ran.
1728    @param test_source_build: Build that contains the server-side test code.
1729    """
1730    if cf_getter is None:
1731        if run_prod_code:
1732            cf_getter = create_fs_getter(_AUTOTEST_DIR)
1733        else:
1734            build = get_test_source_build(
1735                    builds, test_source_build=test_source_build)
1736            cf_getter = _create_ds_getter(build, devserver)
1737    retriever = _get_cf_retriever(cf_getter,
1738                                  run_prod_code=run_prod_code,
1739                                  test_args=test_args)
1740    return retriever.retrieve('dummy_Pass')
1741
1742
1743class _ComposedPredicate(object):
1744    """Return the composition of the predicates.
1745
1746    Predicates are functions that take a test control data object and
1747    return True of that test is to be included.  The returned
1748    predicate's set is the intersection of all of the input predicates'
1749    sets (it returns True if all predicates return True).
1750    """
1751
1752    def __init__(self, predicates):
1753        """Initialize instance.
1754
1755        @param predicates: Iterable of predicates.
1756        """
1757        self._predicates = list(predicates)
1758
1759    def __repr__(self):
1760        return '{cls}({this._predicates!r})'.format(
1761            cls=type(self).__name__,
1762            this=self,
1763        )
1764
1765    def __call__(self, control_data_):
1766        return all(f(control_data_) for f in self._predicates)
1767
1768
1769def _is_nonexistent_board_error(e):
1770    """Return True if error is caused by nonexistent board label.
1771
1772    As of this writing, the particular case we want looks like this:
1773
1774     1) e.problem_keys is a dictionary
1775     2) e.problem_keys['meta_hosts'] exists as the only key
1776        in the dictionary.
1777     3) e.problem_keys['meta_hosts'] matches this pattern:
1778        "Label "board:.*" not found"
1779
1780    We check for conditions 1) and 2) on the
1781    theory that they're relatively immutable.
1782    We don't check condition 3) because it seems
1783    likely to be a maintenance burden, and for the
1784    times when we're wrong, being right shouldn't
1785    matter enough (we _hope_).
1786
1787    @param e: proxy.ValidationError instance
1788    @returns: boolean
1789    """
1790    return (isinstance(e.problem_keys, dict)
1791            and len(e.problem_keys) == 1
1792            and 'meta_hosts' in e.problem_keys)
1793
1794
1795class _ResultReporter(object):
1796    """Abstract base class for reporting test results.
1797
1798    Usually, this is used to report test failures.
1799    """
1800
1801    __metaclass__ = abc.ABCMeta
1802
1803    @abc.abstractmethod
1804    def report(self, result):
1805        """Report test result.
1806
1807        @param result: Status instance for job.
1808        """
1809
1810
1811class MemoryResultReporter(_ResultReporter):
1812    """Reporter that stores results internally for testing."""
1813
1814    def __init__(self):
1815        self.results = []
1816
1817    def report(self, result):
1818        self.results.append(result)
1819
1820
1821class _BugResultReporter(_ResultReporter):
1822    """
1823    Report test results as bugs.
1824    """
1825
1826    def __init__(self, suite, bug_reporter, bug_template):
1827        """
1828        Instantiate instance.
1829
1830        @param suite: _BaseSuite instance
1831        @param bug_reporter: Reporter instance for reporting bugs.
1832        @param bug_template: A template dictionary specifying the default bug
1833                             filing options for failures in this suite.
1834        """
1835        self._suite = suite
1836        self._bug_reporter = bug_reporter
1837        self._bug_template = bug_template
1838
1839    def report(self, result):
1840        self._suite._file_bug(result, self._bug_reporter, self._bug_template)
1841
1842
1843class _EmailResultReporter(_ResultReporter):
1844    """
1845    Report test results as email.
1846
1847    @param suite: _BaseSuite instance
1848    @param bug_template: A template dictionary specifying the default bug
1849                         filing options for failures in this suite.
1850    """
1851
1852    def __init__(self, suite, bug_template):
1853        self._suite = suite
1854        self._bug_template = bug_template
1855
1856    def report(self, result):
1857        # reporting modules have dependency on external
1858        # packages, e.g., httplib2 Such dependency can cause
1859        # issue to any module tries to import suite.py without
1860        # building site-packages first. Since the reporting
1861        # modules are only used in this function, move the
1862        # imports here avoid the requirement of building site
1863        # packages to use other functions in this module.
1864        from autotest_lib.server.cros.dynamic_suite import reporting
1865
1866        reporting.send_email(
1867                self._suite._get_test_bug(result),
1868                self._suite._get_bug_template(result, self._bug_template))
1869