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