Home | History | Annotate | Download | only in dynamic_suite
      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 
      5 import datetime, logging, os, time
      6 
      7 
      8 from autotest_lib.client.common_lib import base_job, global_config, log
      9 from autotest_lib.client.common_lib import time_utils
     10 from autotest_lib.client.common_lib.host_queue_entry_states \
     11     import IntStatus as HqeIntStatus
     12 
     13 DEFAULT_POLL_INTERVAL_SECONDS = 10
     14 
     15 HQE_MAXIMUM_ABORT_RATE_FLOAT = global_config.global_config.get_config_value(
     16             'SCHEDULER', 'hqe_maximum_abort_rate_float', type=float,
     17             default=0.5)
     18 
     19 
     20 def view_is_relevant(view):
     21     """
     22     Indicates whether the view of a given test is meaningful or not.
     23 
     24     @param view: a detailed test 'view' from the TKO DB to look at.
     25     @return True if this is a test result worth looking at further.
     26     """
     27     return not view['test_name'].startswith('CLIENT_JOB')
     28 
     29 
     30 def view_is_for_suite_prep(view):
     31     """
     32     Indicates whether the given test view is the view of Suite prep.
     33 
     34     @param view: a detailed test 'view' from the TKO DB to look at.
     35     @return True if this is view of suite preparation.
     36     """
     37     return view['test_name'] == 'SERVER_JOB'
     38 
     39 
     40 def view_is_for_infrastructure_fail(view):
     41     """
     42     Indicates whether the given test view is from an infra fail.
     43 
     44     @param view: a detailed test 'view' from the TKO DB to look at.
     45     @return True if this view indicates an infrastructure-side issue during
     46                  a test.
     47     """
     48     return view['test_name'].endswith('SERVER_JOB')
     49 
     50 
     51 def is_for_infrastructure_fail(status):
     52     """
     53     Indicates whether the given Status is from an infra fail.
     54 
     55     @param status: the Status object to look at.
     56     @return True if this Status indicates an infrastructure-side issue during
     57                  a test.
     58     """
     59     return view_is_for_infrastructure_fail({'test_name': status.test_name})
     60 
     61 
     62 def gather_job_hostnames(afe, job):
     63     """
     64     Collate and return names of hosts used in |job|.
     65 
     66     @param afe: an instance of AFE as defined in server/frontend.py.
     67     @param job: the job to poll on.
     68     @return iterable of hostnames on which |job| was run, using None as
     69             placeholders.
     70     """
     71     hosts = []
     72     for e in afe.run('get_host_queue_entries', job=job.id):
     73         # If the host queue entry has not yet made it into or past the running
     74         # stage, we should skip it for now.
     75         if (HqeIntStatus.get_value(e['status']) <
     76             HqeIntStatus.get_value(HqeIntStatus.RUNNING)):
     77             hosts.append(None)
     78         elif not e['host']:
     79             logging.warning('Job %s (%s) has an entry with no host!',
     80                          job.name, job.id)
     81             hosts.append(None)
     82         else:
     83             hosts.append(e['host']['hostname'])
     84     return hosts
     85 
     86 
     87 def check_job_abort_status(afe, jobs):
     88     """
     89     Checks the abort status of all the jobs in jobs and if any have too many
     90     aborted HostQueueEntries, return True.
     91 
     92     In the case that any job in jobs has too many aborted host queue entries,
     93     it will raise an exception.
     94 
     95     @param afe: an instance of AFE as defined in server/frontend.py.
     96     @param jobs: an iterable of Running frontend.Jobs
     97 
     98     @returns True if a job in job has too many host queue entries aborted.
     99              False otherwise.
    100     """
    101     for job in jobs:
    102         entries = afe.run('get_host_queue_entries', job=job.id)
    103         num_aborted = 0
    104         for hqe in entries:
    105             if hqe['aborted']:
    106                 num_aborted = num_aborted + 1
    107         if num_aborted > len(entries) * HQE_MAXIMUM_ABORT_RATE_FLOAT:
    108             # This job was not successful, returning True.
    109             logging.error('Too many host queue entries were aborted for job: '
    110                           '%s.', job.id)
    111             return True
    112     return False
    113 
    114 
    115 def _abort_jobs_if_timedout(afe, jobs, start_time, timeout_mins):
    116     """
    117     Abort all of the jobs in jobs if the running time has past the timeout.
    118 
    119     @param afe: an instance of AFE as defined in server/frontend.py.
    120     @param jobs: an iterable of Running frontend.Jobs
    121     @param start_time: Time to compare to the current time to see if a timeout
    122                        has occurred.
    123     @param timeout_mins: Time in minutes to wait before aborting the jobs we
    124                          are waiting on.
    125 
    126     @returns True if we there was a timeout, False if not.
    127     """
    128     if datetime.datetime.utcnow() < (start_time +
    129                                      datetime.timedelta(minutes=timeout_mins)):
    130         return False
    131     for job in jobs:
    132         logging.debug('Job: %s has timed out after %s minutes. Aborting job.',
    133                       job.id, timeout_mins)
    134         afe.run('abort_host_queue_entries', job=job.id)
    135     return True
    136 
    137 
    138 def wait_for_jobs_to_start(afe, jobs, interval=DEFAULT_POLL_INTERVAL_SECONDS,
    139                            start_time=None, wait_timeout_mins=None):
    140     """
    141     Wait for the job specified by |job.id| to start.
    142 
    143     @param afe: an instance of AFE as defined in server/frontend.py.
    144     @param jobs: the jobs to poll on.
    145     @param interval: polling interval in seconds.
    146     @param start_time: Time to compare to the current time to see if a timeout
    147                        has occurred.
    148     @param wait_timeout_mins: Time in minutes to wait before aborting the jobs
    149                                we are waiting on.
    150 
    151     @returns True if the jobs have started, False if they get aborted.
    152     """
    153     if not start_time:
    154         start_time = datetime.datetime.utcnow()
    155     job_ids = [j.id for j in jobs]
    156     while job_ids:
    157         if wait_timeout_mins and _abort_jobs_if_timedout(afe, jobs, start_time,
    158                     wait_timeout_mins):
    159             # The timeout parameter is not None and we have indeed timed out.
    160             return False
    161         for job_id in list(job_ids):
    162             if len(afe.get_jobs(id=job_id, not_yet_run=True)) > 0:
    163                 continue
    164             job_ids.remove(job_id)
    165             logging.debug('Re-imaging job %d running.', job_id)
    166         if job_ids:
    167             logging.debug('Waiting %ds before polling again.', interval)
    168             time.sleep(interval)
    169     return True
    170 
    171 
    172 def wait_for_jobs_to_finish(afe, jobs, interval=DEFAULT_POLL_INTERVAL_SECONDS,
    173                             start_time=None, wait_timeout_mins=None):
    174     """
    175     Wait for the jobs specified by each |job.id| to finish.
    176 
    177     @param afe: an instance of AFE as defined in server/frontend.py.
    178     @param interval: polling interval in seconds.
    179     @param jobs: the jobs to poll on.
    180     @param start_time: Time to compare to the current time to see if a timeout
    181                        has occurred. Defaults to now.
    182     @param wait_timeout_mins: Time in minutes to wait before aborting the jobs
    183                                we are waiting on. Defaults to no timeout.
    184 
    185     @returns True if the jobs have finished, False if they get aborted.
    186     """
    187     if not start_time:
    188         start_time = datetime.datetime.utcnow()
    189     job_ids = [j.id for j in jobs]
    190     while job_ids:
    191         if wait_timeout_mins and _abort_jobs_if_timedout(afe, jobs, start_time,
    192                     wait_timeout_mins):
    193             # The timeout parameter is not None and we have indeed timed out.
    194             return False
    195         for job_id in list(job_ids):
    196             if not afe.get_jobs(id=job_id, finished=True):
    197                 continue
    198             job_ids.remove(job_id)
    199             logging.debug('Re-imaging job %d finished.', job_id)
    200         if job_ids:
    201             logging.debug('Waiting %ds before polling again.', interval)
    202             time.sleep(interval)
    203     return True
    204 
    205 
    206 def wait_for_and_lock_job_hosts(afe, jobs, manager,
    207                                 interval=DEFAULT_POLL_INTERVAL_SECONDS,
    208                                 start_time=None, wait_timeout_mins=None):
    209     """
    210     Poll until devices have begun reimaging, locking them as we go.
    211 
    212     Gather the hosts chosen for |job| -- which must be in the Running
    213     state itself -- and as they each individually come online and begin
    214     Running, lock them.  Poll until all chosen hosts have gone to Running
    215     and been locked using |manager|.
    216 
    217     @param afe: an instance of AFE as defined in server/frontend.py.
    218     @param jobs: an iterable of Running frontend.Jobs
    219     @param manager: a HostLockManager instance.  Hosts will be added to it
    220                     as they start Running, and it will be used to lock them.
    221     @param start_time: Time to compare to the current time to see if a timeout
    222                        has occurred.
    223     @param interval: polling interval.
    224     @param wait_timeout_mins: Time in minutes to wait before aborting the jobs
    225                               we are waiting on.
    226 
    227     @return iterable of the hosts that were locked or None if all the jobs in
    228             jobs have been aborted.
    229     """
    230     def get_all_hosts(my_jobs):
    231         """
    232         Returns a list of all hosts for jobs in my_jobs.
    233 
    234         @param my_jobs: a list of all the jobs we need hostnames for.
    235         @return: a list of hostnames that correspond to my_jobs.
    236         """
    237         all_hosts = []
    238         for job in my_jobs:
    239             all_hosts.extend(gather_job_hostnames(afe, job))
    240         return all_hosts
    241 
    242     if not start_time:
    243         start_time = datetime.datetime.utcnow()
    244     locked_hosts = set()
    245     expected_hosts = set(get_all_hosts(jobs))
    246     logging.debug('Initial expected hosts: %r', expected_hosts)
    247 
    248     while locked_hosts != expected_hosts:
    249         if wait_timeout_mins and _abort_jobs_if_timedout(afe, jobs, start_time,
    250                                                          wait_timeout_mins):
    251             # The timeout parameter is not None and we have timed out.
    252             return locked_hosts
    253         hosts_to_check = [e for e in expected_hosts if e]
    254         if hosts_to_check:
    255             logging.debug('Checking to see if %r are Running.', hosts_to_check)
    256             running_hosts = afe.get_hosts(hosts_to_check, status='Running')
    257             hostnames = [h.hostname for h in running_hosts]
    258             if set(hostnames) - locked_hosts != set():
    259                 # New hosts to lock!
    260                 logging.debug('Locking %r.', hostnames)
    261                 manager.lock(hostnames)
    262             locked_hosts = locked_hosts.union(hostnames)
    263         time.sleep(interval)
    264         # 'None' in expected_hosts means we had entries in the job with no
    265         # host yet assigned, or which weren't Running yet.  We need to forget
    266         # that across loops, though, and remember only hosts we really used.
    267         expected_hosts = expected_hosts.difference([None])
    268 
    269         # get_all_hosts() returns only hosts that are currently Running a
    270         # job we care about.  By unioning with other hosts that we already
    271         # saw, we get the set of all the hosts that have run a job we care
    272         # about.
    273         expected_hosts = expected_hosts.union(get_all_hosts(jobs))
    274         logging.debug('Locked hosts: %r', locked_hosts)
    275         logging.debug('Expected hosts: %r', expected_hosts)
    276 
    277 
    278     return locked_hosts
    279 
    280 
    281 def _collate_aborted(current_value, entry):
    282     """
    283     reduce() over a list of HostQueueEntries for a job; True if any aborted.
    284 
    285     Functor that can be reduced()ed over a list of
    286     HostQueueEntries for a job.  If any were aborted
    287     (|entry.aborted| exists and is True), then the reduce() will
    288     return True.
    289 
    290     Ex:
    291       entries = AFE.run('get_host_queue_entries', job=job.id)
    292       reduce(_collate_aborted, entries, False)
    293 
    294     @param current_value: the current accumulator (a boolean).
    295     @param entry: the current entry under consideration.
    296     @return the value of |entry.aborted| if it exists, False if not.
    297     """
    298     return current_value or ('aborted' in entry and entry['aborted'])
    299 
    300 
    301 def _status_for_test(status):
    302     """
    303     Indicates whether the status of a given test is meaningful or not.
    304 
    305     @param status: frontend.TestStatus object to look at.
    306     @return True if this is a test result worth looking at further.
    307     """
    308     return not (status.test_name.startswith('SERVER_JOB') or
    309                 status.test_name.startswith('CLIENT_JOB'))
    310 
    311 
    312 def _yield_job_results(afe, tko, job):
    313     """
    314     Yields the results of an individual job.
    315 
    316     Yields one Status object per test.
    317 
    318     @param afe: an instance of AFE as defined in server/frontend.py.
    319     @param tko: an instance of TKO as defined in server/frontend.py.
    320     @param job: Job object to get results from, as defined in
    321                 server/frontend.py
    322     @yields an iterator of Statuses, one per test.
    323     """
    324     entries = afe.run('get_host_queue_entries', job=job.id)
    325 
    326     # This query uses the job id to search through the tko_test_view_2
    327     # table, for results of a test with a similar job_tag. The job_tag
    328     # is used to store results, and takes the form job_id-owner/host.
    329     # Many times when a job aborts during a test, the job_tag actually
    330     # exists and the results directory contains valid logs. If the job
    331     # was aborted prematurely i.e before it had a chance to create the
    332     # job_tag, this query will return no results. When statuses is not
    333     # empty it will contain frontend.TestStatus' with fields populated
    334     # using the results of the db query.
    335     statuses = tko.get_job_test_statuses_from_db(job.id)
    336     if not statuses:
    337         yield Status('ABORT', job.name)
    338 
    339     # We only care about the SERVER and CLIENT job failures when there
    340     # are no test failures.
    341     contains_test_failure = any(_status_for_test(s) and s.status != 'GOOD'
    342                                 for s in statuses)
    343     for s in statuses:
    344         # TKO parser uniquelly identifies a test run by
    345         # (test_name, subdir). In dynamic suite, we need to emit
    346         # a subdir for each status and make sure (test_name, subdir)
    347         # in the suite job's status log is unique.
    348         # For non-test status (i.e.SERVER_JOB, CLIENT_JOB),
    349         # we use 'job_tag' from tko_test_view_2, which looks like
    350         # '1246-owner/172.22.33.44'
    351         # For normal test status, we use 'job_tag/subdir'
    352         # which looks like '1246-owner/172.22.33.44/my_DummyTest.tag.subdir_tag'
    353         if _status_for_test(s):
    354             yield Status(s.status, s.test_name, s.reason,
    355                          s.test_started_time, s.test_finished_time,
    356                          job.id, job.owner, s.hostname, job.name,
    357                          subdir=os.path.join(s.job_tag, s.subdir))
    358         else:
    359             if s.status != 'GOOD' and not contains_test_failure:
    360                 yield Status(s.status,
    361                              '%s_%s' % (entries[0]['job']['name'],
    362                                         s.test_name),
    363                              s.reason, s.test_started_time,
    364                              s.test_finished_time, job.id,
    365                              job.owner, s.hostname, job.name,
    366                              subdir=s.job_tag)
    367 
    368 
    369 def wait_for_child_results(afe, tko, parent_job_id):
    370     """
    371     Wait for results of all tests in jobs with given parent id.
    372 
    373     New jobs could be added by calling send(new_jobs) on the generator.
    374     Currently polls for results every 5s.  Yields one Status object per test
    375     as results become available.
    376 
    377     @param afe: an instance of AFE as defined in server/frontend.py.
    378     @param tko: an instance of TKO as defined in server/frontend.py.
    379     @param parent_job_id: Parent job id for the jobs to wait on.
    380     @yields an iterator of Statuses, one per test.
    381     """
    382     remaining_child_jobs = set(job.id for job in
    383                                afe.get_jobs(parent_job_id=parent_job_id))
    384     while remaining_child_jobs:
    385         new_finished_jobs = [job for job in
    386                              afe.get_jobs(parent_job_id=parent_job_id,
    387                                           finished=True)
    388                              if job.id in remaining_child_jobs]
    389 
    390         for job in new_finished_jobs:
    391 
    392             remaining_child_jobs.remove(job.id)
    393             for result in _yield_job_results(afe, tko, job):
    394                 # To figure out what new jobs (like retry jobs) have been
    395                 # created since last iteration, we could re-poll for
    396                 # the set of child jobs in each iteration and
    397                 # calculate the set difference against the set we got in
    398                 # last iteration. As an alternative, we could just make
    399                 # the caller 'send' new jobs to this generator. We go
    400                 # with the latter to avoid unnecessary overhead.
    401                 new_child_jobs = (yield result)
    402                 if new_child_jobs:
    403                     remaining_child_jobs.update([new_job.id for new_job in
    404                                                  new_child_jobs])
    405                     # Return nothing if 'send' is called
    406                     yield None
    407 
    408         time.sleep(5)
    409 
    410 
    411 def wait_for_results(afe, tko, jobs):
    412     """
    413     Wait for results of all tests in all jobs in |jobs|.
    414 
    415     New jobs could be added by calling send(new_jobs) on the generator.
    416     Currently polls for results every 5s.  Yields one Status object per test
    417     as results become available.
    418 
    419     @param afe: an instance of AFE as defined in server/frontend.py.
    420     @param tko: an instance of TKO as defined in server/frontend.py.
    421     @param jobs: a list of Job objects, as defined in server/frontend.py.
    422     @yields an iterator of Statuses, one per test.
    423     """
    424     local_jobs = list(jobs)
    425     while local_jobs:
    426         for job in list(local_jobs):
    427             if not afe.get_jobs(id=job.id, finished=True):
    428                 continue
    429 
    430             local_jobs.remove(job)
    431             for result in _yield_job_results(afe, tko, job):
    432                 # The caller can 'send' new jobs (i.e. retry jobs)
    433                 # to this generator at any time.
    434                 new_jobs = (yield result)
    435                 if new_jobs:
    436                     local_jobs.extend(new_jobs)
    437                     # Return nothing if 'send' is called
    438                     yield None
    439 
    440         time.sleep(5)
    441 
    442 
    443 def gather_per_host_results(afe, tko, jobs, name_prefix=''):
    444     """
    445     Gather currently-available results for all |jobs|, aggregated per-host.
    446 
    447     For each job in |jobs|, gather per-host results and summarize into a single
    448     log entry.  For example, a FAILed SERVER_JOB and successful actual test
    449     is reported as a FAIL.
    450 
    451     @param afe: an instance of AFE as defined in server/frontend.py.
    452     @param tko: an instance of TKO as defined in server/frontend.py.
    453     @param jobs: a list of Job objects, as defined in server/frontend.py.
    454     @param name_prefix: optional string to prepend to Status object names.
    455     @return a dict mapping {hostname: Status}, one per host used in a Job.
    456     """
    457     to_return = {}
    458     for job in jobs:
    459         for s in tko.get_job_test_statuses_from_db(job.id):
    460             candidate = Status(s.status,
    461                                name_prefix+s.hostname,
    462                                s.reason,
    463                                s.test_started_time,
    464                                s.test_finished_time)
    465             if (s.hostname not in to_return or
    466                 candidate.is_worse_than(to_return[s.hostname])):
    467                 to_return[s.hostname] = candidate
    468 
    469         # If we didn't find more specific data above for a host, fill in here.
    470         # For jobs that didn't even make it to finding a host, just collapse
    471         # into a single log entry.
    472         for e in afe.run('get_host_queue_entries', job=job.id):
    473             host = e['host']['hostname'] if e['host'] else 'hostless' + job.name
    474             if host not in to_return:
    475                 to_return[host] = Status(Status.STATUS_MAP[e['status']],
    476                                          job.name,
    477                                          'Did not run',
    478                                          begin_time_str=job.created_on)
    479 
    480     return to_return
    481 
    482 
    483 def check_and_record_reimage_results(per_host_statuses, group, record_entry):
    484     """
    485     Record all Statuses in results and return True if at least one was GOOD.
    486 
    487     @param per_host_statuses: dict mapping {hostname: Status}, one per host
    488                               used in a Job.
    489     @param group: the HostGroup used for the Job whose results we're reporting.
    490     @param record_entry: a callable to use for logging.
    491                prototype:
    492                    record_entry(base_job.status_log_entry)
    493     @return True if at least one of the Statuses are good.
    494     """
    495     failures = []
    496     for hostname, status in per_host_statuses.iteritems():
    497         if status.is_good():
    498             group.mark_host_success(hostname)
    499             status.record_all(record_entry)
    500         else:
    501             failures.append(status)
    502 
    503     success = group.enough_hosts_succeeded()
    504     if success:
    505         for failure in failures:
    506             logging.warning("%s failed to reimage.", failure.test_name)
    507             failure.override_status('WARN')
    508             failure.record_all(record_entry)
    509     else:
    510         for failure in failures:
    511             # No need to log warnings; the job is failing.
    512             failure.record_all(record_entry)
    513 
    514     return success
    515 
    516 
    517 class Status(object):
    518     """
    519     A class representing a test result.
    520 
    521     Stores all pertinent info about a test result and, given a callable
    522     to use, can record start, result, and end info appropriately.
    523 
    524     @var _status: status code, e.g. 'INFO', 'FAIL', etc.
    525     @var _test_name: the name of the test whose result this is.
    526     @var _reason: message explaining failure, if any.
    527     @var _begin_timestamp: when test started (int, in seconds since the epoch).
    528     @var _end_timestamp: when test finished (int, in seconds since the epoch).
    529     @var _id: the ID of the job that generated this Status.
    530     @var _owner: the owner of the job that generated this Status.
    531 
    532     @var STATUS_MAP: a dict mapping host queue entry status strings to canonical
    533                      status codes; e.g. 'Aborted' -> 'ABORT'
    534     """
    535     _status = None
    536     _test_name = None
    537     _reason = None
    538     _begin_timestamp = None
    539     _end_timestamp = None
    540 
    541     # Queued status can occur if the try job just aborted due to not completing
    542     # reimaging for all machines. The Queued corresponds to an 'ABORT'.
    543     STATUS_MAP = {'Failed': 'FAIL', 'Aborted': 'ABORT', 'Completed': 'GOOD',
    544                   'Queued' : 'ABORT'}
    545 
    546     class sle(base_job.status_log_entry):
    547         """
    548         Thin wrapper around status_log_entry that supports stringification.
    549         """
    550         def __str__(self):
    551             return self.render()
    552 
    553         def __repr__(self):
    554             return self.render()
    555 
    556 
    557     def __init__(self, status, test_name, reason='', begin_time_str=None,
    558                  end_time_str=None, job_id=None, owner=None, hostname=None,
    559                  job_name='', subdir=None):
    560         """
    561         Constructor
    562 
    563         @param status: status code, e.g. 'INFO', 'FAIL', etc.
    564         @param test_name: the name of the test whose result this is.
    565         @param reason: message explaining failure, if any; Optional.
    566         @param begin_time_str: when test started (in time_utils.TIME_FMT);
    567                                now() if None or 'None'.
    568         @param end_time_str: when test finished (in time_utils.TIME_FMT);
    569                              now() if None or 'None'.
    570         @param job_id: the ID of the job that generated this Status.
    571         @param owner: the owner of the job that generated this Status.
    572         @param hostname: The name of the host the test that generated this
    573                          result ran on.
    574         @param job_name: The job name; Contains the test name with/without the
    575                          experimental prefix, the tag and the build.
    576         @param subdir: The result directory of the test. It will be recorded
    577                        as the subdir in the status.log file.
    578         """
    579         self._status = status
    580         self._test_name = test_name
    581         self._reason = reason
    582         self._id = job_id
    583         self._owner = owner
    584         self._hostname = hostname
    585         self._job_name = job_name
    586         self._subdir = subdir
    587         # Autoserv drops a keyval of the started time which eventually makes its
    588         # way here.  Therefore, if we have a starting time, we may assume that
    589         # the test reached Running and actually began execution on a drone.
    590         self._test_executed = begin_time_str and begin_time_str != 'None'
    591 
    592         if begin_time_str and begin_time_str != 'None':
    593             self._begin_timestamp = int(time.mktime(
    594                 datetime.datetime.strptime(
    595                     begin_time_str, time_utils.TIME_FMT).timetuple()))
    596         else:
    597             self._begin_timestamp = int(time.time())
    598 
    599         if end_time_str and end_time_str != 'None':
    600             self._end_timestamp = int(time.mktime(
    601                 datetime.datetime.strptime(
    602                     end_time_str, time_utils.TIME_FMT).timetuple()))
    603         else:
    604             self._end_timestamp = int(time.time())
    605 
    606 
    607     def is_good(self):
    608         """ Returns true if status is good. """
    609         return self._status == 'GOOD'
    610 
    611 
    612     def is_warn(self):
    613         """ Returns true if status is warn. """
    614         return self._status == 'WARN'
    615 
    616 
    617     def is_testna(self):
    618         """ Returns true if status is TEST_NA """
    619         return self._status == 'TEST_NA'
    620 
    621 
    622     def is_worse_than(self, candidate):
    623         """
    624         Return whether |self| represents a "worse" failure than |candidate|.
    625 
    626         "Worse" is defined the same as it is for log message purposes in
    627         common_lib/log.py.  We also consider status with a specific error
    628         message to represent a "worse" failure than one without.
    629 
    630         @param candidate: a Status instance to compare to this one.
    631         @return True if |self| is "worse" than |candidate|.
    632         """
    633         if self._status != candidate._status:
    634             return (log.job_statuses.index(self._status) <
    635                     log.job_statuses.index(candidate._status))
    636         # else, if the statuses are the same...
    637         if self._reason and not candidate._reason:
    638             return True
    639         return False
    640 
    641 
    642     def record_start(self, record_entry):
    643         """
    644         Use record_entry to log message about start of test.
    645 
    646         @param record_entry: a callable to use for logging.
    647                prototype:
    648                    record_entry(base_job.status_log_entry)
    649         """
    650         log_entry = Status.sle('START', self._subdir,
    651                                 self._test_name, '',
    652                                 None, self._begin_timestamp)
    653         record_entry(log_entry, log_in_subdir=False)
    654 
    655 
    656     def record_result(self, record_entry):
    657         """
    658         Use record_entry to log message about result of test.
    659 
    660         @param record_entry: a callable to use for logging.
    661                prototype:
    662                    record_entry(base_job.status_log_entry)
    663         """
    664         log_entry = Status.sle(self._status, self._subdir,
    665                                 self._test_name, self._reason, None,
    666                                 self._end_timestamp)
    667         record_entry(log_entry, log_in_subdir=False)
    668 
    669 
    670     def record_end(self, record_entry):
    671         """
    672         Use record_entry to log message about end of test.
    673 
    674         @param record_entry: a callable to use for logging.
    675                prototype:
    676                    record_entry(base_job.status_log_entry)
    677         """
    678         log_entry = Status.sle('END %s' % self._status, self._subdir,
    679                                self._test_name, '', None, self._end_timestamp)
    680         record_entry(log_entry, log_in_subdir=False)
    681 
    682 
    683     def record_all(self, record_entry):
    684         """
    685         Use record_entry to log all messages about test results.
    686 
    687         @param record_entry: a callable to use for logging.
    688                prototype:
    689                    record_entry(base_job.status_log_entry)
    690         """
    691         self.record_start(record_entry)
    692         self.record_result(record_entry)
    693         self.record_end(record_entry)
    694 
    695 
    696     def override_status(self, override):
    697         """
    698         Override the _status field of this Status.
    699 
    700         @param override: value with which to override _status.
    701         """
    702         self._status = override
    703 
    704 
    705     @property
    706     def test_name(self):
    707         """ Name of the test this status corresponds to. """
    708         return self._test_name
    709 
    710 
    711     @test_name.setter
    712     def test_name(self, value):
    713         """
    714         Test name setter.
    715 
    716         @param value: The test name.
    717         """
    718         self._test_name = value
    719 
    720 
    721     @property
    722     def id(self):
    723         """ Id of the job that corresponds to this status. """
    724         return self._id
    725 
    726 
    727     @property
    728     def owner(self):
    729         """ Owner of the job that corresponds to this status. """
    730         return self._owner
    731 
    732 
    733     @property
    734     def hostname(self):
    735         """ Host the job corresponding to this status ran on. """
    736         return self._hostname
    737 
    738 
    739     @property
    740     def reason(self):
    741         """ Reason the job corresponding to this status failed. """
    742         return self._reason
    743 
    744 
    745     @property
    746     def test_executed(self):
    747         """ If the test reached running an autoserv instance or not. """
    748         return self._test_executed
    749 
    750     @property
    751     def subdir(self):
    752         """Subdir of test this status corresponds to."""
    753         return self._subdir
    754