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