• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2016 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import collections
16import contextlib
17import copy
18import functools
19import inspect
20import logging
21import os
22import sys
23
24from mobly import controller_manager
25from mobly import expects
26from mobly import records
27from mobly import runtime_test_info
28from mobly import signals
29from mobly import utils
30
31# Macro strings for test result reporting.
32TEST_CASE_TOKEN = '[Test]'
33RESULT_LINE_TEMPLATE = TEST_CASE_TOKEN + ' %s %s'
34
35TEST_STAGE_BEGIN_LOG_TEMPLATE = '[{parent_token}]#{child_token} >>> BEGIN >>>'
36TEST_STAGE_END_LOG_TEMPLATE = '[{parent_token}]#{child_token} <<< END <<<'
37
38# Names of execution stages, in the order they happen during test runs.
39STAGE_NAME_PRE_RUN = 'pre_run'
40# Deprecated, use `STAGE_NAME_PRE_RUN` instead.
41STAGE_NAME_SETUP_GENERATED_TESTS = 'setup_generated_tests'
42STAGE_NAME_SETUP_CLASS = 'setup_class'
43STAGE_NAME_SETUP_TEST = 'setup_test'
44STAGE_NAME_TEARDOWN_TEST = 'teardown_test'
45STAGE_NAME_TEARDOWN_CLASS = 'teardown_class'
46STAGE_NAME_CLEAN_UP = 'clean_up'
47
48# Attribute names
49ATTR_REPEAT_CNT = '_repeat_count'
50ATTR_MAX_RETRY_CNT = '_max_retry_count'
51ATTR_MAX_CONSEC_ERROR = '_max_consecutive_error'
52
53
54class Error(Exception):
55  """Raised for exceptions that occurred in BaseTestClass."""
56
57
58def repeat(count, max_consecutive_error=None):
59  """Decorator for repeating a test case multiple times.
60
61  The BaseTestClass will execute the test cases annotated with this decorator
62  the specified number of time.
63
64  This decorator only stores the information needed for the repeat. It does not
65  execute the repeat.
66
67  Args:
68    count: int, the total number of times to execute the decorated test case.
69    max_consecutive_error: int, the maximum number of consecutively failed
70      iterations allowed. If reached, the remaining iterations is abandoned.
71      By default this is not enabled.
72
73  Returns:
74    The wrapped test function.
75
76  Raises:
77    ValueError, if the user input is invalid.
78  """
79  if count <= 1:
80    raise ValueError(
81        f'The `count` for `repeat` must be larger than 1, got "{count}".')
82
83  if max_consecutive_error is not None and max_consecutive_error > count:
84    raise ValueError(
85        f'The `max_consecutive_error` ({max_consecutive_error}) for `repeat` '
86        f'must be smaller than `count` ({count}).')
87
88  def _outer_decorator(func):
89    setattr(func, ATTR_REPEAT_CNT, count)
90    setattr(func, ATTR_MAX_CONSEC_ERROR, max_consecutive_error)
91
92    @functools.wraps(func)
93    def _wrapper(*args):
94      func(*args)
95
96    return _wrapper
97
98  return _outer_decorator
99
100
101def retry(max_count):
102  """Decorator for retrying a test case until it passes.
103
104  The BaseTestClass will keep executing the test cases annotated with this
105  decorator until the test passes, or the maxinum number of iterations have
106  been met.
107
108  This decorator only stores the information needed for the retry. It does not
109  execute the retry.
110
111  Args:
112    max_count: int, the maximum number of times to execute the decorated test
113      case.
114
115  Returns:
116    The wrapped test function.
117
118  Raises:
119    ValueError, if the user input is invalid.
120  """
121  if max_count <= 1:
122    raise ValueError(
123        f'The `max_count` for `retry` must be larger than 1, got "{max_count}".'
124    )
125
126  def _outer_decorator(func):
127    setattr(func, ATTR_MAX_RETRY_CNT, max_count)
128
129    @functools.wraps(func)
130    def _wrapper(*args):
131      func(*args)
132
133    return _wrapper
134
135  return _outer_decorator
136
137
138class BaseTestClass:
139  """Base class for all test classes to inherit from.
140
141  This class gets all the controller objects from test_runner and executes
142  the tests requested within itself.
143
144  Most attributes of this class are set at runtime based on the configuration
145  provided.
146
147  The default logger in logging module is set up for each test run. If you
148  want to log info to the test run output file, use `logging` directly, like
149  `logging.info`.
150
151  Attributes:
152    tests: A list of strings, each representing a test method name.
153    TAG: A string used to refer to a test class. Default is the test class
154      name.
155    results: A records.TestResult object for aggregating test results from
156      the execution of tests.
157    controller_configs: dict, controller configs provided by the user via
158      test bed config.
159    current_test_info: RuntimeTestInfo, runtime information on the test
160      currently being executed.
161    root_output_path: string, storage path for output files associated with
162      the entire test run. A test run can have multiple test class
163      executions. This includes the test summary and Mobly log files.
164    log_path: string, storage path for files specific to a single test
165      class execution.
166    test_bed_name: [Deprecated, use 'testbed_name' instead]
167      string, the name of the test bed used by a test run.
168    testbed_name: string, the name of the test bed used by a test run.
169    user_params: dict, custom parameters from user, to be consumed by
170      the test logic.
171  """
172
173  TAG = None
174
175  def __init__(self, configs):
176    """Constructor of BaseTestClass.
177
178    The constructor takes a config_parser.TestRunConfig object and which has
179    all the information needed to execute this test class, like log_path
180    and controller configurations. For details, see the definition of class
181    config_parser.TestRunConfig.
182
183    Args:
184      configs: A config_parser.TestRunConfig object.
185    """
186    self.tests = []
187    class_identifier = self.__class__.__name__
188    if configs.test_class_name_suffix:
189      class_identifier = '%s_%s' % (class_identifier,
190                                    configs.test_class_name_suffix)
191    if self.TAG is None:
192      self.TAG = class_identifier
193    # Set params.
194    self.root_output_path = configs.log_path
195    self.log_path = os.path.join(self.root_output_path, class_identifier)
196    utils.create_dir(self.log_path)
197    # Deprecated, use 'testbed_name'
198    self.test_bed_name = configs.test_bed_name
199    self.testbed_name = configs.testbed_name
200    self.user_params = configs.user_params
201    self.results = records.TestResult()
202    self.summary_writer = configs.summary_writer
203    self._generated_test_table = collections.OrderedDict()
204    self._controller_manager = controller_manager.ControllerManager(
205        class_name=self.TAG, controller_configs=configs.controller_configs)
206    self.controller_configs = self._controller_manager.controller_configs
207
208  def unpack_userparams(self,
209                        req_param_names=None,
210                        opt_param_names=None,
211                        **kwargs):
212    """An optional function that unpacks user defined parameters into
213    individual variables.
214
215    After unpacking, the params can be directly accessed with self.xxx.
216
217    If a required param is not provided, an exception is raised. If an
218    optional param is not provided, a warning line will be logged.
219
220    To provide a param, add it in the config file or pass it in as a kwarg.
221    If a param appears in both the config file and kwarg, the value in the
222    config file is used.
223
224    User params from the config file can also be directly accessed in
225    self.user_params.
226
227    Args:
228      req_param_names: A list of names of the required user params.
229      opt_param_names: A list of names of the optional user params.
230      **kwargs: Arguments that provide default values.
231        e.g. unpack_userparams(required_list, opt_list, arg_a='hello')
232        self.arg_a will be 'hello' unless it is specified again in
233        required_list or opt_list.
234
235    Raises:
236      Error: A required user params is not provided.
237    """
238    req_param_names = req_param_names or []
239    opt_param_names = opt_param_names or []
240    for k, v in kwargs.items():
241      if k in self.user_params:
242        v = self.user_params[k]
243      setattr(self, k, v)
244    for name in req_param_names:
245      if hasattr(self, name):
246        continue
247      if name not in self.user_params:
248        raise Error('Missing required user param "%s" in test '
249                    'configuration.' % name)
250      setattr(self, name, self.user_params[name])
251    for name in opt_param_names:
252      if hasattr(self, name):
253        continue
254      if name in self.user_params:
255        setattr(self, name, self.user_params[name])
256      else:
257        logging.warning(
258            'Missing optional user param "%s" in '
259            'configuration, continue.', name)
260
261  def register_controller(self, module, required=True, min_number=1):
262    """Loads a controller module and returns its loaded devices.
263
264    A Mobly controller module is a Python lib that can be used to control
265    a device, service, or equipment. To be Mobly compatible, a controller
266    module needs to have the following members:
267
268    .. code-block:: python
269
270      def create(configs):
271        [Required] Creates controller objects from configurations.
272
273        Args:
274          configs: A list of serialized data like string/dict. Each
275            element of the list is a configuration for a controller
276            object.
277
278        Returns:
279          A list of objects.
280
281      def destroy(objects):
282        [Required] Destroys controller objects created by the create
283        function. Each controller object shall be properly cleaned up
284        and all the resources held should be released, e.g. memory
285        allocation, sockets, file handlers etc.
286
287        Args:
288          A list of controller objects created by the create function.
289
290      def get_info(objects):
291        [Optional] Gets info from the controller objects used in a test
292        run. The info will be included in test_summary.yaml under
293        the key 'ControllerInfo'. Such information could include unique
294        ID, version, or anything that could be useful for describing the
295        test bed and debugging.
296
297        Args:
298          objects: A list of controller objects created by the create
299            function.
300
301        Returns:
302          A list of json serializable objects: each represents the
303            info of a controller object. The order of the info
304            object should follow that of the input objects.
305
306    Registering a controller module declares a test class's dependency the
307    controller. If the module config exists and the module matches the
308    controller interface, controller objects will be instantiated with
309    corresponding configs. The module should be imported first.
310
311    Args:
312      module: A module that follows the controller module interface.
313      required: A bool. If True, failing to register the specified
314        controller module raises exceptions. If False, the objects
315        failed to instantiate will be skipped.
316      min_number: An integer that is the minimum number of controller
317        objects to be created. Default is one, since you should not
318        register a controller module without expecting at least one
319        object.
320
321    Returns:
322      A list of controller objects instantiated from controller_module, or
323      None if no config existed for this controller and it was not a
324      required controller.
325
326    Raises:
327      ControllerError:
328        * The controller module has already been registered.
329        * The actual number of objects instantiated is less than the
330        * `min_number`.
331        * `required` is True and no corresponding config can be found.
332        * Any other error occurred in the registration process.
333    """
334    return self._controller_manager.register_controller(module, required,
335                                                        min_number)
336
337  def _record_controller_info(self):
338    # Collect controller information and write to test result.
339    for record in self._controller_manager.get_controller_info_records():
340      self.results.add_controller_info_record(record)
341      self.summary_writer.dump(record.to_dict(),
342                               records.TestSummaryEntryType.CONTROLLER_INFO)
343
344  def _pre_run(self):
345    """Proxy function to guarantee the base implementation of `pre_run` is
346    called.
347
348    Returns:
349      True if setup is successful, False otherwise.
350    """
351    stage_name = STAGE_NAME_PRE_RUN
352    record = records.TestResultRecord(stage_name, self.TAG)
353    record.test_begin()
354    self.current_test_info = runtime_test_info.RuntimeTestInfo(
355        stage_name, self.log_path, record)
356    try:
357      with self._log_test_stage(stage_name):
358        self.pre_run()
359      # TODO(angli): Remove this context block after the full deprecation of
360      # `setup_generated_tests`.
361      with self._log_test_stage(stage_name):
362        self.setup_generated_tests()
363      return True
364    except Exception as e:
365      logging.exception('%s failed for %s.', stage_name, self.TAG)
366      record.test_error(e)
367      self.results.add_class_error(record)
368      self.summary_writer.dump(record.to_dict(),
369                               records.TestSummaryEntryType.RECORD)
370      return False
371
372  def pre_run(self):
373    """Preprocesses that need to be done before setup_class.
374
375    This phase is used to do pre-test processes like generating tests.
376    This is the only place `self.generate_tests` should be called.
377
378    If this function throws an error, the test class will be marked failure
379    and the "Requested" field will be 0 because the number of tests
380    requested is unknown at this point.
381    """
382
383  def setup_generated_tests(self):
384    """[DEPRECATED] Use `pre_run` instead.
385
386    Preprocesses that need to be done before setup_class.
387
388    This phase is used to do pre-test processes like generating tests.
389    This is the only place `self.generate_tests` should be called.
390
391    If this function throws an error, the test class will be marked failure
392    and the "Requested" field will be 0 because the number of tests
393    requested is unknown at this point.
394    """
395
396  def _setup_class(self):
397    """Proxy function to guarantee the base implementation of setup_class
398    is called.
399
400    Returns:
401      If `self.results` is returned instead of None, this means something
402      has gone wrong, and the rest of the test class should not execute.
403    """
404    # Setup for the class.
405    class_record = records.TestResultRecord(STAGE_NAME_SETUP_CLASS, self.TAG)
406    class_record.test_begin()
407    self.current_test_info = runtime_test_info.RuntimeTestInfo(
408        STAGE_NAME_SETUP_CLASS, self.log_path, class_record)
409    expects.recorder.reset_internal_states(class_record)
410    try:
411      with self._log_test_stage(STAGE_NAME_SETUP_CLASS):
412        self.setup_class()
413    except signals.TestAbortSignal:
414      # Throw abort signals to outer try block for handling.
415      raise
416    except Exception as e:
417      # Setup class failed for unknown reasons.
418      # Fail the class and skip all tests.
419      logging.exception('Error in %s#setup_class.', self.TAG)
420      class_record.test_error(e)
421      self.results.add_class_error(class_record)
422      self._exec_procedure_func(self._on_fail, class_record)
423      class_record.update_record()
424      self.summary_writer.dump(class_record.to_dict(),
425                               records.TestSummaryEntryType.RECORD)
426      self._skip_remaining_tests(e)
427      return self.results
428    if expects.recorder.has_error:
429      self._exec_procedure_func(self._on_fail, class_record)
430      class_record.test_error()
431      class_record.update_record()
432      self.summary_writer.dump(class_record.to_dict(),
433                               records.TestSummaryEntryType.RECORD)
434      self.results.add_class_error(class_record)
435      self._skip_remaining_tests(class_record.termination_signal.exception)
436      return self.results
437
438  def setup_class(self):
439    """Setup function that will be called before executing any test in the
440    class.
441
442    To signal setup failure, use asserts or raise your own exception.
443
444    Errors raised from `setup_class` will trigger `on_fail`.
445
446    Implementation is optional.
447    """
448
449  def _teardown_class(self):
450    """Proxy function to guarantee the base implementation of
451    teardown_class is called.
452    """
453    stage_name = STAGE_NAME_TEARDOWN_CLASS
454    record = records.TestResultRecord(stage_name, self.TAG)
455    record.test_begin()
456    self.current_test_info = runtime_test_info.RuntimeTestInfo(
457        stage_name, self.log_path, record)
458    expects.recorder.reset_internal_states(record)
459    try:
460      with self._log_test_stage(stage_name):
461        self.teardown_class()
462    except signals.TestAbortAll as e:
463      setattr(e, 'results', self.results)
464      raise
465    except Exception as e:
466      logging.exception('Error encountered in %s.', stage_name)
467      record.test_error(e)
468      record.update_record()
469      self.results.add_class_error(record)
470      self.summary_writer.dump(record.to_dict(),
471                               records.TestSummaryEntryType.RECORD)
472    else:
473      if expects.recorder.has_error:
474        record.test_error()
475        record.update_record()
476        self.results.add_class_error(record)
477        self.summary_writer.dump(record.to_dict(),
478                                 records.TestSummaryEntryType.RECORD)
479    finally:
480      self._clean_up()
481
482  def teardown_class(self):
483    """Teardown function that will be called after all the selected tests in
484    the test class have been executed.
485
486    Errors raised from `teardown_class` do not trigger `on_fail`.
487
488    Implementation is optional.
489    """
490
491  @contextlib.contextmanager
492  def _log_test_stage(self, stage_name):
493    """Logs the begin and end of a test stage.
494
495    This context adds two log lines meant for clarifying the boundary of
496    each execution stage in Mobly log.
497
498    Args:
499      stage_name: string, name of the stage to log.
500    """
501    parent_token = self.current_test_info.name
502    # If the name of the stage is the same as the test name, in which case
503    # the stage is class-level instead of test-level, use the class's
504    # reference tag as the parent token instead.
505    if parent_token == stage_name:
506      parent_token = self.TAG
507    logging.debug(
508        TEST_STAGE_BEGIN_LOG_TEMPLATE.format(parent_token=parent_token,
509                                             child_token=stage_name))
510    try:
511      yield
512    finally:
513      logging.debug(
514          TEST_STAGE_END_LOG_TEMPLATE.format(parent_token=parent_token,
515                                             child_token=stage_name))
516
517  def _setup_test(self, test_name):
518    """Proxy function to guarantee the base implementation of setup_test is
519    called.
520    """
521    with self._log_test_stage(STAGE_NAME_SETUP_TEST):
522      self.setup_test()
523
524  def setup_test(self):
525    """Setup function that will be called every time before executing each
526    test method in the test class.
527
528    To signal setup failure, use asserts or raise your own exception.
529
530    Implementation is optional.
531    """
532
533  def _teardown_test(self, test_name):
534    """Proxy function to guarantee the base implementation of teardown_test
535    is called.
536    """
537    with self._log_test_stage(STAGE_NAME_TEARDOWN_TEST):
538      self.teardown_test()
539
540  def teardown_test(self):
541    """Teardown function that will be called every time a test method has
542    been executed.
543
544    Implementation is optional.
545    """
546
547  def _on_fail(self, record):
548    """Proxy function to guarantee the base implementation of on_fail is
549    called.
550
551    Args:
552      record: records.TestResultRecord, a copy of the test record for
553          this test, containing all information of the test execution
554          including exception objects.
555    """
556    self.on_fail(record)
557
558  def on_fail(self, record):
559    """A function that is executed upon a test failure.
560
561    User implementation is optional.
562
563    Args:
564      record: records.TestResultRecord, a copy of the test record for
565        this test, containing all information of the test execution
566        including exception objects.
567    """
568
569  def _on_pass(self, record):
570    """Proxy function to guarantee the base implementation of on_pass is
571    called.
572
573    Args:
574      record: records.TestResultRecord, a copy of the test record for
575        this test, containing all information of the test execution
576        including exception objects.
577    """
578    msg = record.details
579    if msg:
580      logging.info(msg)
581    self.on_pass(record)
582
583  def on_pass(self, record):
584    """A function that is executed upon a test passing.
585
586    Implementation is optional.
587
588    Args:
589      record: records.TestResultRecord, a copy of the test record for
590        this test, containing all information of the test execution
591        including exception objects.
592    """
593
594  def _on_skip(self, record):
595    """Proxy function to guarantee the base implementation of on_skip is
596    called.
597
598    Args:
599      record: records.TestResultRecord, a copy of the test record for
600        this test, containing all information of the test execution
601        including exception objects.
602    """
603    logging.info('Reason to skip: %s', record.details)
604    logging.info(RESULT_LINE_TEMPLATE, record.test_name, record.result)
605    self.on_skip(record)
606
607  def on_skip(self, record):
608    """A function that is executed upon a test being skipped.
609
610    Implementation is optional.
611
612    Args:
613      record: records.TestResultRecord, a copy of the test record for
614        this test, containing all information of the test execution
615        including exception objects.
616    """
617
618  def _exec_procedure_func(self, func, tr_record):
619    """Executes a procedure function like on_pass, on_fail etc.
620
621    This function will alter the 'Result' of the test's record if
622    exceptions happened when executing the procedure function, but
623    prevents procedure functions from altering test records themselves
624    by only passing in a copy.
625
626    This will let signals.TestAbortAll through so abort_all works in all
627    procedure functions.
628
629    Args:
630      func: The procedure function to be executed.
631      tr_record: The TestResultRecord object associated with the test
632        executed.
633    """
634    func_name = func.__name__
635    procedure_name = func_name[1:] if func_name[0] == '_' else func_name
636    with self._log_test_stage(procedure_name):
637      try:
638        # Pass a copy of the record instead of the actual object so that it
639        # will not be modified.
640        func(copy.deepcopy(tr_record))
641      except signals.TestAbortSignal:
642        raise
643      except Exception as e:
644        logging.exception('Exception happened when executing %s for %s.',
645                          procedure_name, self.current_test_info.name)
646        tr_record.add_error(procedure_name, e)
647
648  def record_data(self, content):
649    """Record an entry in test summary file.
650
651    Sometimes additional data need to be recorded in summary file for
652    debugging or post-test analysis.
653
654    Each call adds a new entry to the summary file, with no guarantee of
655    its position among the summary file entries.
656
657    The content should be a dict. If absent, timestamp field is added for
658    ease of parsing later.
659
660    Args:
661      content: dict, the data to add to summary file.
662    """
663    if 'timestamp' not in content:
664      content = content.copy()
665      content['timestamp'] = utils.get_current_epoch_time()
666    self.summary_writer.dump(content, records.TestSummaryEntryType.USER_DATA)
667
668  def _exec_one_test_with_retry(self, test_name, test_method, max_count):
669    """Executes one test and retry the test if needed.
670
671    Repeatedly execute a test case until it passes or the maximum count of
672    iteration has been reached.
673
674    Args:
675      test_name: string, Name of the test.
676      test_method: function, The test method to execute.
677      max_count: int, the maximum number of iterations to execute the test for.
678    """
679
680    def should_retry(record):
681      return record.result in [
682          records.TestResultEnums.TEST_RESULT_FAIL,
683          records.TestResultEnums.TEST_RESULT_ERROR,
684      ]
685
686    previous_record = self.exec_one_test(test_name, test_method)
687
688    if not should_retry(previous_record):
689      return
690
691    for i in range(max_count - 1):
692      retry_name = f'{test_name}_retry_{i+1}'
693      new_record = records.TestResultRecord(retry_name, self.TAG)
694      new_record.retry_parent = previous_record
695      previous_record = self.exec_one_test(retry_name, test_method, new_record)
696      if not should_retry(previous_record):
697        break
698
699  def _exec_one_test_with_repeat(self, test_name, test_method, repeat_count,
700                                 max_consecutive_error):
701    """Repeatedly execute a test case.
702
703    This method performs the action defined by the `repeat` decorator.
704
705    If the number of consecutive failures reach the threshold set by
706    `max_consecutive_error`, the remaining iterations will be abandoned.
707
708    Args:
709      test_name: string, Name of the test.
710      test_method: function, The test method to execute.
711      repeat_count: int, the number of times to repeat the test case.
712      max_consecutive_error: int, the maximum number of consecutive iterations
713        allowed to fail before abandoning the remaining iterations.
714    """
715
716    consecutive_error_count = 0
717
718    # If max_consecutive_error is not set by user, it is considered the same as
719    # the repeat_count.
720    if max_consecutive_error == 0:
721      max_consecutive_error = repeat_count
722
723    for i in range(repeat_count):
724      new_test_name = f'{test_name}_{i}'
725      record = self.exec_one_test(new_test_name, test_method)
726      if record.result in [
727          records.TestResultEnums.TEST_RESULT_FAIL,
728          records.TestResultEnums.TEST_RESULT_ERROR,
729      ]:
730        consecutive_error_count += 1
731      else:
732        consecutive_error_count = 0
733      if consecutive_error_count == max_consecutive_error:
734        logging.error(
735            'Repeated test case "%s" has consecutively failed %d iterations, '
736            'aborting the remaining %d iterations.', test_name,
737            consecutive_error_count, repeat_count - 1 - i)
738        return
739
740  def exec_one_test(self, test_name, test_method, record=None):
741    """Executes one test and update test results.
742
743    Executes setup_test, the test method, and teardown_test; then creates a
744    records.TestResultRecord object with the execution information and adds
745    the record to the test class's test results.
746
747    Args:
748      test_name: string, Name of the test.
749      test_method: function, The test method to execute.
750      record: records.TestResultRecord, optional arg for injecting a record
751        object to use for this test execution. If not set, a new one is created
752        created. This is meant for passing information between consecutive test
753        case execution for retry purposes. Do NOT abuse this for "magical"
754        features.
755
756    Returns:
757      TestResultRecord, the test result record object of the test execution.
758      This object is strictly for read-only purposes. Modifying this record
759      will not change what is reported in the test run's summary yaml file.
760    """
761    tr_record = record or records.TestResultRecord(test_name, self.TAG)
762    tr_record.uid = getattr(test_method, 'uid', None)
763    tr_record.test_begin()
764    self.current_test_info = runtime_test_info.RuntimeTestInfo(
765        test_name, self.log_path, tr_record)
766    expects.recorder.reset_internal_states(tr_record)
767    logging.info('%s %s', TEST_CASE_TOKEN, test_name)
768    # Did teardown_test throw an error.
769    teardown_test_failed = False
770    try:
771      try:
772        try:
773          self._setup_test(test_name)
774        except signals.TestFailure as e:
775          _, _, traceback = sys.exc_info()
776          raise signals.TestError(e.details, e.extras).with_traceback(traceback)
777        test_method()
778      except (signals.TestPass, signals.TestAbortSignal, signals.TestSkip):
779        raise
780      except Exception:
781        logging.exception('Exception occurred in %s.',
782                          self.current_test_info.name)
783        raise
784      finally:
785        before_count = expects.recorder.error_count
786        try:
787          self._teardown_test(test_name)
788        except signals.TestAbortSignal:
789          raise
790        except Exception as e:
791          logging.exception('Exception occurred in %s of %s.',
792                            STAGE_NAME_TEARDOWN_TEST,
793                            self.current_test_info.name)
794          tr_record.test_error()
795          tr_record.add_error(STAGE_NAME_TEARDOWN_TEST, e)
796          teardown_test_failed = True
797        else:
798          # Check if anything failed by `expects`.
799          if before_count < expects.recorder.error_count:
800            teardown_test_failed = True
801    except (signals.TestFailure, AssertionError) as e:
802      tr_record.test_fail(e)
803    except signals.TestSkip as e:
804      # Test skipped.
805      tr_record.test_skip(e)
806    except signals.TestAbortSignal as e:
807      # Abort signals, pass along.
808      tr_record.test_fail(e)
809      raise
810    except signals.TestPass as e:
811      # Explicit test pass.
812      tr_record.test_pass(e)
813    except Exception as e:
814      # Exception happened during test.
815      tr_record.test_error(e)
816    else:
817      # No exception is thrown from test and teardown, if `expects` has
818      # error, the test should fail with the first error in `expects`.
819      if expects.recorder.has_error and not teardown_test_failed:
820        tr_record.test_fail()
821      # Otherwise the test passed.
822      elif not teardown_test_failed:
823        tr_record.test_pass()
824    finally:
825      tr_record.update_record()
826      try:
827        if tr_record.result in (records.TestResultEnums.TEST_RESULT_ERROR,
828                                records.TestResultEnums.TEST_RESULT_FAIL):
829          self._exec_procedure_func(self._on_fail, tr_record)
830        elif tr_record.result == records.TestResultEnums.TEST_RESULT_PASS:
831          self._exec_procedure_func(self._on_pass, tr_record)
832        elif tr_record.result == records.TestResultEnums.TEST_RESULT_SKIP:
833          self._exec_procedure_func(self._on_skip, tr_record)
834      finally:
835        logging.info(RESULT_LINE_TEMPLATE, tr_record.test_name,
836                     tr_record.result)
837        self.results.add_record(tr_record)
838        self.summary_writer.dump(tr_record.to_dict(),
839                                 records.TestSummaryEntryType.RECORD)
840        self.current_test_info = None
841    return tr_record
842
843  def _assert_function_names_in_stack(self, expected_func_names):
844    """Asserts that the current stack contains any of the given function names.
845    """
846    current_frame = inspect.currentframe()
847    caller_frames = inspect.getouterframes(current_frame, 2)
848    for caller_frame in caller_frames[2:]:
849      if caller_frame[3] in expected_func_names:
850        return
851    raise Error(f"'{caller_frames[1][3]}' cannot be called outside of the "
852                f"following functions: {expected_func_names}.")
853
854  def generate_tests(self, test_logic, name_func, arg_sets, uid_func=None):
855    """Generates tests in the test class.
856
857    This function has to be called inside a test class's `self.pre_run` or
858    `self.setup_generated_tests`.
859
860    Generated tests are not written down as methods, but as a list of
861    parameter sets. This way we reduce code repetition and improve test
862    scalability.
863
864    Users can provide an optional function to specify the UID of each test.
865    Not all generated tests are required to have UID.
866
867    Args:
868      test_logic: function, the common logic shared by all the generated
869        tests.
870      name_func: function, generate a test name according to a set of
871        test arguments. This function should take the same arguments as
872        the test logic function.
873      arg_sets: a list of tuples, each tuple is a set of arguments to be
874        passed to the test logic function and name function.
875      uid_func: function, an optional function that takes the same
876        arguments as the test logic function and returns a string that
877        is the corresponding UID.
878    """
879    self._assert_function_names_in_stack(
880        [STAGE_NAME_PRE_RUN, STAGE_NAME_SETUP_GENERATED_TESTS])
881    root_msg = 'During test generation of "%s":' % test_logic.__name__
882    for args in arg_sets:
883      test_name = name_func(*args)
884      if test_name in self.get_existing_test_names():
885        raise Error('%s Test name "%s" already exists, cannot be duplicated!' %
886                    (root_msg, test_name))
887      test_func = functools.partial(test_logic, *args)
888      # If the `test_logic` method is decorated by `retry` or `repeat`
889      # decorators, copy the attributes added by the decorators to the
890      # generated test methods as well, so the generated test methods
891      # also have the retry/repeat behavior.
892      for attr_name in (ATTR_MAX_RETRY_CNT, ATTR_MAX_CONSEC_ERROR,
893                        ATTR_REPEAT_CNT):
894        attr = getattr(test_logic, attr_name, None)
895        if attr is not None:
896          setattr(test_func, attr_name, attr)
897      if uid_func is not None:
898        uid = uid_func(*args)
899        if uid is None:
900          logging.warning('%s UID for arg set %s is None.', root_msg, args)
901        else:
902          setattr(test_func, 'uid', uid)
903      self._generated_test_table[test_name] = test_func
904
905  def _safe_exec_func(self, func, *args):
906    """Executes a function with exception safeguard.
907
908    This will let signals.TestAbortAll through so abort_all works in all
909    procedure functions.
910
911    Args:
912      func: Function to be executed.
913      args: Arguments to be passed to the function.
914
915    Returns:
916      Whatever the function returns.
917    """
918    try:
919      return func(*args)
920    except signals.TestAbortAll:
921      raise
922    except Exception:
923      logging.exception('Exception happened when executing %s in %s.',
924                        func.__name__, self.TAG)
925
926  def get_existing_test_names(self):
927    """Gets the names of existing tests in the class.
928
929    A method in the class is considered a test if its name starts with
930    'test_*'.
931
932    Note this only gets the names of tests that already exist. If
933    `generate_tests` has not happened when this was called, the
934    generated tests won't be listed.
935
936    Returns:
937      A list of strings, each is a test method name.
938    """
939    test_names = []
940    for name, _ in inspect.getmembers(type(self), callable):
941      if name.startswith('test_'):
942        test_names.append(name)
943    return test_names + list(self._generated_test_table.keys())
944
945  def _get_test_methods(self, test_names):
946    """Resolves test method names to bound test methods.
947
948    Args:
949      test_names: A list of strings, each string is a test method name.
950
951    Returns:
952      A list of tuples of (string, function). String is the test method
953      name, function is the actual python method implementing its logic.
954
955    Raises:
956      Error: The test name does not follow naming convention 'test_*'.
957        This can only be caused by user input.
958    """
959    test_methods = []
960    for test_name in test_names:
961      if not test_name.startswith('test_'):
962        raise Error('Test method name %s does not follow naming '
963                    'convention test_*, abort.' % test_name)
964      if hasattr(self, test_name):
965        test_method = getattr(self, test_name)
966      elif test_name in self._generated_test_table:
967        test_method = self._generated_test_table[test_name]
968      else:
969        raise Error('%s does not have test method %s.' % (self.TAG, test_name))
970      test_methods.append((test_name, test_method))
971    return test_methods
972
973  def _skip_remaining_tests(self, exception):
974    """Marks any requested test that has not been executed in a class as
975    skipped.
976
977    This is useful for handling abort class signal.
978
979    Args:
980      exception: The exception object that was thrown to trigger the
981        skip.
982    """
983    for test_name in self.results.requested:
984      if not self.results.is_test_executed(test_name):
985        test_record = records.TestResultRecord(test_name, self.TAG)
986        test_record.test_skip(exception)
987        self.results.add_record(test_record)
988        self.summary_writer.dump(test_record.to_dict(),
989                                 records.TestSummaryEntryType.RECORD)
990
991  def run(self, test_names=None):
992    """Runs tests within a test class.
993
994    One of these test method lists will be executed, shown here in priority
995    order:
996
997    1. The test_names list, which is passed from cmd line. Invalid names
998       are guarded by cmd line arg parsing.
999    2. The self.tests list defined in test class. Invalid names are
1000       ignored.
1001    3. All function that matches test method naming convention in the test
1002       class.
1003
1004    Args:
1005      test_names: A list of string that are test method names requested in
1006        cmd line.
1007
1008    Returns:
1009      The test results object of this class.
1010    """
1011    logging.log_path = self.log_path
1012    # Executes pre-setup procedures, like generating test methods.
1013    if not self._pre_run():
1014      return self.results
1015    logging.info('==========> %s <==========', self.TAG)
1016    # Devise the actual test methods to run in the test class.
1017    if not test_names:
1018      if self.tests:
1019        # Specified by run list in class.
1020        test_names = list(self.tests)
1021      else:
1022        # No test method specified by user, execute all in test class.
1023        test_names = self.get_existing_test_names()
1024    self.results.requested = test_names
1025    self.summary_writer.dump(self.results.requested_test_names_dict(),
1026                             records.TestSummaryEntryType.TEST_NAME_LIST)
1027    tests = self._get_test_methods(test_names)
1028    try:
1029      setup_class_result = self._setup_class()
1030      if setup_class_result:
1031        return setup_class_result
1032      # Run tests in order.
1033      for test_name, test_method in tests:
1034        max_consecutive_error = getattr(test_method, ATTR_MAX_CONSEC_ERROR, 0)
1035        repeat_count = getattr(test_method, ATTR_REPEAT_CNT, 0)
1036        max_retry_count = getattr(test_method, ATTR_MAX_RETRY_CNT, 0)
1037        if max_retry_count:
1038          self._exec_one_test_with_retry(test_name, test_method,
1039                                         max_retry_count)
1040        elif repeat_count:
1041          self._exec_one_test_with_repeat(test_name, test_method, repeat_count,
1042                                          max_consecutive_error)
1043        else:
1044          self.exec_one_test(test_name, test_method)
1045      return self.results
1046    except signals.TestAbortClass as e:
1047      e.details = 'Test class aborted due to: %s' % e.details
1048      self._skip_remaining_tests(e)
1049      return self.results
1050    except signals.TestAbortAll as e:
1051      e.details = 'All remaining tests aborted due to: %s' % e.details
1052      self._skip_remaining_tests(e)
1053      # Piggy-back test results on this exception object so we don't lose
1054      # results from this test class.
1055      setattr(e, 'results', self.results)
1056      raise e
1057    finally:
1058      self._teardown_class()
1059      logging.info('Summary for test class %s: %s', self.TAG,
1060                   self.results.summary_str())
1061
1062  def _clean_up(self):
1063    """The final stage of a test class execution."""
1064    stage_name = STAGE_NAME_CLEAN_UP
1065    record = records.TestResultRecord(stage_name, self.TAG)
1066    record.test_begin()
1067    self.current_test_info = runtime_test_info.RuntimeTestInfo(
1068        stage_name, self.log_path, record)
1069    expects.recorder.reset_internal_states(record)
1070    with self._log_test_stage(stage_name):
1071      # Write controller info and summary to summary file.
1072      self._record_controller_info()
1073      self._controller_manager.unregister_controllers()
1074      if expects.recorder.has_error:
1075        record.test_error()
1076        record.update_record()
1077        self.results.add_class_error(record)
1078        self.summary_writer.dump(record.to_dict(),
1079                                 records.TestSummaryEntryType.RECORD)
1080