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
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      # TODO(angli): Remove this context block after the full deprecation of
374      # `setup_generated_tests`.
375      with self._log_test_stage(stage_name):
376        self.setup_generated_tests()
377      return True
378    except Exception as e:
379      logging.exception('%s failed for %s.', stage_name, self.TAG)
380      record.test_error(e)
381      self.results.add_class_error(record)
382      self.summary_writer.dump(
383          record.to_dict(), records.TestSummaryEntryType.RECORD
384      )
385      return False
386
387  def pre_run(self):
388    """Preprocesses that need to be done before setup_class.
389
390    This phase is used to do pre-test processes like generating tests.
391    This is the only place `self.generate_tests` should be called.
392
393    If this function throws an error, the test class will be marked failure
394    and the "Requested" field will be 0 because the number of tests
395    requested is unknown at this point.
396    """
397
398  def setup_generated_tests(self):
399    """[DEPRECATED] Use `pre_run` instead.
400
401    Preprocesses that need to be done before setup_class.
402
403    This phase is used to do pre-test processes like generating tests.
404    This is the only place `self.generate_tests` should be called.
405
406    If this function throws an error, the test class will be marked failure
407    and the "Requested" field will be 0 because the number of tests
408    requested is unknown at this point.
409    """
410
411  def _setup_class(self):
412    """Proxy function to guarantee the base implementation of setup_class
413    is called.
414
415    Returns:
416      If `self.results` is returned instead of None, this means something
417      has gone wrong, and the rest of the test class should not execute.
418    """
419    # Setup for the class.
420    class_record = records.TestResultRecord(STAGE_NAME_SETUP_CLASS, self.TAG)
421    class_record.test_begin()
422    self.current_test_info = runtime_test_info.RuntimeTestInfo(
423        STAGE_NAME_SETUP_CLASS, self.log_path, class_record
424    )
425    expects.recorder.reset_internal_states(class_record)
426    try:
427      with self._log_test_stage(STAGE_NAME_SETUP_CLASS):
428        self.setup_class()
429    except signals.TestAbortSignal:
430      # Throw abort signals to outer try block for handling.
431      raise
432    except Exception as e:
433      # Setup class failed for unknown reasons.
434      # Fail the class and skip all tests.
435      logging.exception('Error in %s#setup_class.', self.TAG)
436      class_record.test_error(e)
437      self.results.add_class_error(class_record)
438      self._exec_procedure_func(self._on_fail, class_record)
439      class_record.update_record()
440      self.summary_writer.dump(
441          class_record.to_dict(), records.TestSummaryEntryType.RECORD
442      )
443      self._skip_remaining_tests(e)
444      return self.results
445    if expects.recorder.has_error:
446      self._exec_procedure_func(self._on_fail, class_record)
447      class_record.test_error()
448      class_record.update_record()
449      self.summary_writer.dump(
450          class_record.to_dict(), records.TestSummaryEntryType.RECORD
451      )
452      self.results.add_class_error(class_record)
453      self._skip_remaining_tests(class_record.termination_signal.exception)
454      return self.results
455
456  def setup_class(self):
457    """Setup function that will be called before executing any test in the
458    class.
459
460    To signal setup failure, use asserts or raise your own exception.
461
462    Errors raised from `setup_class` will trigger `on_fail`.
463
464    Implementation is optional.
465    """
466
467  def _teardown_class(self):
468    """Proxy function to guarantee the base implementation of
469    teardown_class is called.
470    """
471    stage_name = STAGE_NAME_TEARDOWN_CLASS
472    record = records.TestResultRecord(stage_name, self.TAG)
473    record.test_begin()
474    self.current_test_info = runtime_test_info.RuntimeTestInfo(
475        stage_name, self.log_path, record
476    )
477    expects.recorder.reset_internal_states(record)
478    try:
479      with self._log_test_stage(stage_name):
480        self.teardown_class()
481    except signals.TestAbortAll as e:
482      setattr(e, 'results', self.results)
483      raise
484    except Exception as e:
485      logging.exception('Error encountered in %s.', stage_name)
486      record.test_error(e)
487      record.update_record()
488      self.results.add_class_error(record)
489      self.summary_writer.dump(
490          record.to_dict(), records.TestSummaryEntryType.RECORD
491      )
492    else:
493      if expects.recorder.has_error:
494        record.test_error()
495        record.update_record()
496        self.results.add_class_error(record)
497        self.summary_writer.dump(
498            record.to_dict(), records.TestSummaryEntryType.RECORD
499        )
500    finally:
501      self._clean_up()
502
503  def teardown_class(self):
504    """Teardown function that will be called after all the selected tests in
505    the test class have been executed.
506
507    Errors raised from `teardown_class` do not trigger `on_fail`.
508
509    Implementation is optional.
510    """
511
512  @contextlib.contextmanager
513  def _log_test_stage(self, stage_name):
514    """Logs the begin and end of a test stage.
515
516    This context adds two log lines meant for clarifying the boundary of
517    each execution stage in Mobly log.
518
519    Args:
520      stage_name: string, name of the stage to log.
521    """
522    parent_token = self.current_test_info.name
523    # If the name of the stage is the same as the test name, in which case
524    # the stage is class-level instead of test-level, use the class's
525    # reference tag as the parent token instead.
526    if parent_token == stage_name:
527      parent_token = self.TAG
528    logging.debug(
529        TEST_STAGE_BEGIN_LOG_TEMPLATE.format(
530            parent_token=parent_token, child_token=stage_name
531        )
532    )
533    try:
534      yield
535    finally:
536      logging.debug(
537          TEST_STAGE_END_LOG_TEMPLATE.format(
538              parent_token=parent_token, child_token=stage_name
539          )
540      )
541
542  def _setup_test(self, test_name):
543    """Proxy function to guarantee the base implementation of setup_test is
544    called.
545    """
546    with self._log_test_stage(STAGE_NAME_SETUP_TEST):
547      self.setup_test()
548
549  def setup_test(self):
550    """Setup function that will be called every time before executing each
551    test method in the test class.
552
553    To signal setup failure, use asserts or raise your own exception.
554
555    Implementation is optional.
556    """
557
558  def _teardown_test(self, test_name):
559    """Proxy function to guarantee the base implementation of teardown_test
560    is called.
561    """
562    with self._log_test_stage(STAGE_NAME_TEARDOWN_TEST):
563      self.teardown_test()
564
565  def teardown_test(self):
566    """Teardown function that will be called every time a test method has
567    been executed.
568
569    Implementation is optional.
570    """
571
572  def _on_fail(self, record):
573    """Proxy function to guarantee the base implementation of on_fail is
574    called.
575
576    Args:
577      record: records.TestResultRecord, a copy of the test record for
578          this test, containing all information of the test execution
579          including exception objects.
580    """
581    self.on_fail(record)
582
583  def on_fail(self, record):
584    """A function that is executed upon a test failure.
585
586    User 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_pass(self, record):
595    """Proxy function to guarantee the base implementation of on_pass 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    msg = record.details
604    if msg:
605      logging.info(msg)
606    self.on_pass(record)
607
608  def on_pass(self, record):
609    """A function that is executed upon a test passing.
610
611    Implementation is optional.
612
613    Args:
614      record: records.TestResultRecord, a copy of the test record for
615        this test, containing all information of the test execution
616        including exception objects.
617    """
618
619  def _on_skip(self, record):
620    """Proxy function to guarantee the base implementation of on_skip is
621    called.
622
623    Args:
624      record: records.TestResultRecord, a copy of the test record for
625        this test, containing all information of the test execution
626        including exception objects.
627    """
628    logging.info('Reason to skip: %s', record.details)
629    logging.info(RESULT_LINE_TEMPLATE, record.test_name, record.result)
630    self.on_skip(record)
631
632  def on_skip(self, record):
633    """A function that is executed upon a test being skipped.
634
635    Implementation is optional.
636
637    Args:
638      record: records.TestResultRecord, a copy of the test record for
639        this test, containing all information of the test execution
640        including exception objects.
641    """
642
643  def _exec_procedure_func(self, func, tr_record):
644    """Executes a procedure function like on_pass, on_fail etc.
645
646    This function will alter the 'Result' of the test's record if
647    exceptions happened when executing the procedure function, but
648    prevents procedure functions from altering test records themselves
649    by only passing in a copy.
650
651    This will let signals.TestAbortAll through so abort_all works in all
652    procedure functions.
653
654    Args:
655      func: The procedure function to be executed.
656      tr_record: The TestResultRecord object associated with the test
657        executed.
658    """
659    func_name = func.__name__
660    procedure_name = func_name[1:] if func_name[0] == '_' else func_name
661    with self._log_test_stage(procedure_name):
662      try:
663        # Pass a copy of the record instead of the actual object so that it
664        # will not be modified.
665        func(copy.deepcopy(tr_record))
666      except signals.TestAbortSignal:
667        raise
668      except Exception as e:
669        logging.exception(
670            'Exception happened when executing %s for %s.',
671            procedure_name,
672            self.current_test_info.name,
673        )
674        tr_record.add_error(procedure_name, e)
675
676  def record_data(self, content):
677    """Record an entry in test summary file.
678
679    Sometimes additional data need to be recorded in summary file for
680    debugging or post-test analysis.
681
682    Each call adds a new entry to the summary file, with no guarantee of
683    its position among the summary file entries.
684
685    The content should be a dict. If absent, timestamp field is added for
686    ease of parsing later.
687
688    Args:
689      content: dict, the data to add to summary file.
690    """
691    if 'timestamp' not in content:
692      content = content.copy()
693      content['timestamp'] = utils.get_current_epoch_time()
694    self.summary_writer.dump(content, records.TestSummaryEntryType.USER_DATA)
695
696  def _exec_one_test_with_retry(self, test_name, test_method, max_count):
697    """Executes one test and retry the test if needed.
698
699    Repeatedly execute a test case until it passes or the maximum count of
700    iteration has been reached.
701
702    Args:
703      test_name: string, Name of the test.
704      test_method: function, The test method to execute.
705      max_count: int, the maximum number of iterations to execute the test for.
706    """
707
708    def should_retry(record):
709      return record.result in [
710          records.TestResultEnums.TEST_RESULT_FAIL,
711          records.TestResultEnums.TEST_RESULT_ERROR,
712      ]
713
714    previous_record = self.exec_one_test(test_name, test_method)
715
716    if not should_retry(previous_record):
717      return
718
719    for i in range(max_count - 1):
720      retry_name = f'{test_name}_retry_{i+1}'
721      new_record = records.TestResultRecord(retry_name, self.TAG)
722      new_record.retry_parent = previous_record
723      new_record.parent = (previous_record, records.TestParentType.RETRY)
724      previous_record = self.exec_one_test(retry_name, test_method, new_record)
725      if not should_retry(previous_record):
726        break
727
728  def _exec_one_test_with_repeat(
729      self, test_name, test_method, repeat_count, max_consecutive_error
730  ):
731    """Repeatedly execute a test case.
732
733    This method performs the action defined by the `repeat` decorator.
734
735    If the number of consecutive failures reach the threshold set by
736    `max_consecutive_error`, the remaining iterations will be abandoned.
737
738    Args:
739      test_name: string, Name of the test.
740      test_method: function, The test method to execute.
741      repeat_count: int, the number of times to repeat the test case.
742      max_consecutive_error: int, the maximum number of consecutive iterations
743        allowed to fail before abandoning the remaining iterations.
744    """
745
746    consecutive_error_count = 0
747
748    # If max_consecutive_error is not set by user, it is considered the same as
749    # the repeat_count.
750    if max_consecutive_error == 0:
751      max_consecutive_error = repeat_count
752
753    previous_record = None
754    for i in range(repeat_count):
755      new_test_name = f'{test_name}_{i}'
756      new_record = records.TestResultRecord(new_test_name, self.TAG)
757      if i > 0:
758        new_record.parent = (previous_record, records.TestParentType.REPEAT)
759      previous_record = self.exec_one_test(
760          new_test_name, test_method, new_record
761      )
762      if previous_record.result in [
763          records.TestResultEnums.TEST_RESULT_FAIL,
764          records.TestResultEnums.TEST_RESULT_ERROR,
765      ]:
766        consecutive_error_count += 1
767      else:
768        consecutive_error_count = 0
769
770      if consecutive_error_count == max_consecutive_error:
771        logging.error(
772            'Repeated test case "%s" has consecutively failed %d iterations, '
773            'aborting the remaining %d iterations.',
774            test_name,
775            consecutive_error_count,
776            repeat_count - 1 - i,
777        )
778        return
779
780  def exec_one_test(self, test_name, test_method, record=None):
781    """Executes one test and update test results.
782
783    Executes setup_test, the test method, and teardown_test; then creates a
784    records.TestResultRecord object with the execution information and adds
785    the record to the test class's test results.
786
787    Args:
788      test_name: string, Name of the test.
789      test_method: function, The test method to execute.
790      record: records.TestResultRecord, optional arg for injecting a record
791        object to use for this test execution. If not set, a new one is created
792        created. This is meant for passing information between consecutive test
793        case execution for retry purposes. Do NOT abuse this for "magical"
794        features.
795
796    Returns:
797      TestResultRecord, the test result record object of the test execution.
798      This object is strictly for read-only purposes. Modifying this record
799      will not change what is reported in the test run's summary yaml file.
800    """
801    tr_record = record or records.TestResultRecord(test_name, self.TAG)
802    tr_record.uid = getattr(test_method, 'uid', None)
803    tr_record.test_begin()
804    self.current_test_info = runtime_test_info.RuntimeTestInfo(
805        test_name, self.log_path, tr_record
806    )
807    expects.recorder.reset_internal_states(tr_record)
808    logging.info('%s %s', TEST_CASE_TOKEN, test_name)
809    # Did teardown_test throw an error.
810    teardown_test_failed = False
811    try:
812      try:
813        try:
814          self._setup_test(test_name)
815        except signals.TestFailure as e:
816          _, _, traceback = sys.exc_info()
817          raise signals.TestError(e.details, e.extras).with_traceback(traceback)
818        test_method()
819      except (signals.TestPass, signals.TestAbortSignal, signals.TestSkip):
820        raise
821      except Exception:
822        logging.exception(
823            'Exception occurred in %s.', self.current_test_info.name
824        )
825        raise
826      finally:
827        before_count = expects.recorder.error_count
828        try:
829          self._teardown_test(test_name)
830        except signals.TestAbortSignal:
831          raise
832        except Exception as e:
833          logging.exception(
834              'Exception occurred in %s of %s.',
835              STAGE_NAME_TEARDOWN_TEST,
836              self.current_test_info.name,
837          )
838          tr_record.test_error()
839          tr_record.add_error(STAGE_NAME_TEARDOWN_TEST, e)
840          teardown_test_failed = True
841        else:
842          # Check if anything failed by `expects`.
843          if before_count < expects.recorder.error_count:
844            tr_record.test_error()
845            teardown_test_failed = True
846    except (signals.TestFailure, AssertionError) as e:
847      tr_record.test_fail(e)
848    except signals.TestSkip as e:
849      # Test skipped.
850      tr_record.test_skip(e)
851    except signals.TestAbortSignal as e:
852      # Abort signals, pass along.
853      tr_record.test_fail(e)
854      raise
855    except signals.TestPass as e:
856      # Explicit test pass.
857      tr_record.test_pass(e)
858    except Exception as e:
859      # Exception happened during test.
860      tr_record.test_error(e)
861    else:
862      # No exception is thrown from test and teardown, if `expects` has
863      # error, the test should fail with the first error in `expects`.
864      if expects.recorder.has_error and not teardown_test_failed:
865        tr_record.test_fail()
866      # Otherwise the test passed.
867      elif not teardown_test_failed:
868        tr_record.test_pass()
869    finally:
870      tr_record.update_record()
871      try:
872        if tr_record.result in (
873            records.TestResultEnums.TEST_RESULT_ERROR,
874            records.TestResultEnums.TEST_RESULT_FAIL,
875        ):
876          self._exec_procedure_func(self._on_fail, tr_record)
877        elif tr_record.result == records.TestResultEnums.TEST_RESULT_PASS:
878          self._exec_procedure_func(self._on_pass, tr_record)
879        elif tr_record.result == records.TestResultEnums.TEST_RESULT_SKIP:
880          self._exec_procedure_func(self._on_skip, tr_record)
881      finally:
882        logging.info(
883            RESULT_LINE_TEMPLATE, tr_record.test_name, tr_record.result
884        )
885        self.results.add_record(tr_record)
886        self.summary_writer.dump(
887            tr_record.to_dict(), records.TestSummaryEntryType.RECORD
888        )
889        self.current_test_info = None
890    return tr_record
891
892  def _assert_function_names_in_stack(self, expected_func_names):
893    """Asserts that the current stack contains any of the given function names."""
894    current_frame = inspect.currentframe()
895    caller_frames = inspect.getouterframes(current_frame, 2)
896    for caller_frame in caller_frames[2:]:
897      if caller_frame[3] in expected_func_names:
898        return
899    raise Error(
900        f"'{caller_frames[1][3]}' cannot be called outside of the "
901        f'following functions: {expected_func_names}.'
902    )
903
904  def generate_tests(self, test_logic, name_func, arg_sets, uid_func=None):
905    """Generates tests in the test class.
906
907    This function has to be called inside a test class's `self.pre_run` or
908    `self.setup_generated_tests`.
909
910    Generated tests are not written down as methods, but as a list of
911    parameter sets. This way we reduce code repetition and improve test
912    scalability.
913
914    Users can provide an optional function to specify the UID of each test.
915    Not all generated tests are required to have UID.
916
917    Args:
918      test_logic: function, the common logic shared by all the generated
919        tests.
920      name_func: function, generate a test name according to a set of
921        test arguments. This function should take the same arguments as
922        the test logic function.
923      arg_sets: a list of tuples, each tuple is a set of arguments to be
924        passed to the test logic function and name function.
925      uid_func: function, an optional function that takes the same
926        arguments as the test logic function and returns a string that
927        is the corresponding UID.
928    """
929    self._assert_function_names_in_stack(
930        [STAGE_NAME_PRE_RUN, STAGE_NAME_SETUP_GENERATED_TESTS]
931    )
932    root_msg = 'During test generation of "%s":' % test_logic.__name__
933    for args in arg_sets:
934      test_name = name_func(*args)
935      if test_name in self.get_existing_test_names():
936        raise Error(
937            '%s Test name "%s" already exists, cannot be duplicated!'
938            % (root_msg, test_name)
939        )
940      test_func = functools.partial(test_logic, *args)
941      # If the `test_logic` method is decorated by `retry` or `repeat`
942      # decorators, copy the attributes added by the decorators to the
943      # generated test methods as well, so the generated test methods
944      # also have the retry/repeat behavior.
945      for attr_name in (
946          ATTR_MAX_RETRY_CNT,
947          ATTR_MAX_CONSEC_ERROR,
948          ATTR_REPEAT_CNT,
949      ):
950        attr = getattr(test_logic, attr_name, None)
951        if attr is not None:
952          setattr(test_func, attr_name, attr)
953      if uid_func is not None:
954        uid = uid_func(*args)
955        if uid is None:
956          logging.warning('%s UID for arg set %s is None.', root_msg, args)
957        else:
958          setattr(test_func, 'uid', uid)
959      self._generated_test_table[test_name] = test_func
960
961  def _safe_exec_func(self, func, *args):
962    """Executes a function with exception safeguard.
963
964    This will let signals.TestAbortAll through so abort_all works in all
965    procedure functions.
966
967    Args:
968      func: Function to be executed.
969      args: Arguments to be passed to the function.
970
971    Returns:
972      Whatever the function returns.
973    """
974    try:
975      return func(*args)
976    except signals.TestAbortAll:
977      raise
978    except Exception:
979      logging.exception(
980          'Exception happened when executing %s in %s.', func.__name__, self.TAG
981      )
982
983  def get_existing_test_names(self):
984    """Gets the names of existing tests in the class.
985
986    A method in the class is considered a test if its name starts with
987    'test_*'.
988
989    Note this only gets the names of tests that already exist. If
990    `generate_tests` has not happened when this was called, the
991    generated tests won't be listed.
992
993    Returns:
994      A list of strings, each is a test method name.
995    """
996    test_names = []
997    for name, _ in inspect.getmembers(type(self), callable):
998      if name.startswith('test_'):
999        test_names.append(name)
1000    return test_names + list(self._generated_test_table.keys())
1001
1002  def _get_test_methods(self, test_names):
1003    """Resolves test method names to bound test methods.
1004
1005    Args:
1006      test_names: A list of strings, each string is a test method name.
1007
1008    Returns:
1009      A list of tuples of (string, function). String is the test method
1010      name, function is the actual python method implementing its logic.
1011
1012    Raises:
1013      Error: The test name does not follow naming convention 'test_*'.
1014        This can only be caused by user input.
1015    """
1016    test_methods = []
1017    for test_name in test_names:
1018      if not test_name.startswith('test_'):
1019        raise Error(
1020            'Test method name %s does not follow naming '
1021            'convention test_*, abort.' % test_name
1022        )
1023      if hasattr(self, test_name):
1024        test_method = getattr(self, test_name)
1025      elif test_name in self._generated_test_table:
1026        test_method = self._generated_test_table[test_name]
1027      else:
1028        raise Error('%s does not have test method %s.' % (self.TAG, test_name))
1029      test_methods.append((test_name, test_method))
1030    return test_methods
1031
1032  def _skip_remaining_tests(self, exception):
1033    """Marks any requested test that has not been executed in a class as
1034    skipped.
1035
1036    This is useful for handling abort class signal.
1037
1038    Args:
1039      exception: The exception object that was thrown to trigger the
1040        skip.
1041    """
1042    for test_name in self.results.requested:
1043      if not self.results.is_test_executed(test_name):
1044        test_record = records.TestResultRecord(test_name, self.TAG)
1045        test_record.test_skip(exception)
1046        self.results.add_record(test_record)
1047        self.summary_writer.dump(
1048            test_record.to_dict(), records.TestSummaryEntryType.RECORD
1049        )
1050
1051  def run(self, test_names=None):
1052    """Runs tests within a test class.
1053
1054    One of these test method lists will be executed, shown here in priority
1055    order:
1056
1057    1. The test_names list, which is passed from cmd line. Invalid names
1058       are guarded by cmd line arg parsing.
1059    2. The self.tests list defined in test class. Invalid names are
1060       ignored.
1061    3. All function that matches test method naming convention in the test
1062       class.
1063
1064    Args:
1065      test_names: A list of string that are test method names requested in
1066        cmd line.
1067
1068    Returns:
1069      The test results object of this class.
1070    """
1071    logging.log_path = self.log_path
1072    # Executes pre-setup procedures, like generating test methods.
1073    if not self._pre_run():
1074      return self.results
1075    logging.info('==========> %s <==========', self.TAG)
1076    # Devise the actual test methods to run in the test class.
1077    if not test_names:
1078      if self.tests:
1079        # Specified by run list in class.
1080        test_names = list(self.tests)
1081      else:
1082        # No test method specified by user, execute all in test class.
1083        test_names = self.get_existing_test_names()
1084    self.results.requested = test_names
1085    self.summary_writer.dump(
1086        self.results.requested_test_names_dict(),
1087        records.TestSummaryEntryType.TEST_NAME_LIST,
1088    )
1089    tests = self._get_test_methods(test_names)
1090    try:
1091      setup_class_result = self._setup_class()
1092      if setup_class_result:
1093        return setup_class_result
1094      # Run tests in order.
1095      for test_name, test_method in tests:
1096        max_consecutive_error = getattr(test_method, ATTR_MAX_CONSEC_ERROR, 0)
1097        repeat_count = getattr(test_method, ATTR_REPEAT_CNT, 0)
1098        max_retry_count = getattr(test_method, ATTR_MAX_RETRY_CNT, 0)
1099        if max_retry_count:
1100          self._exec_one_test_with_retry(
1101              test_name, test_method, max_retry_count
1102          )
1103        elif repeat_count:
1104          self._exec_one_test_with_repeat(
1105              test_name, test_method, repeat_count, max_consecutive_error
1106          )
1107        else:
1108          self.exec_one_test(test_name, test_method)
1109      return self.results
1110    except signals.TestAbortClass as e:
1111      e.details = 'Test class aborted due to: %s' % e.details
1112      self._skip_remaining_tests(e)
1113      return self.results
1114    except signals.TestAbortAll as e:
1115      e.details = 'All remaining tests aborted due to: %s' % e.details
1116      self._skip_remaining_tests(e)
1117      # Piggy-back test results on this exception object so we don't lose
1118      # results from this test class.
1119      setattr(e, 'results', self.results)
1120      raise e
1121    finally:
1122      self._teardown_class()
1123      logging.info(
1124          'Summary for test class %s: %s', self.TAG, self.results.summary_str()
1125      )
1126
1127  def _clean_up(self):
1128    """The final stage of a test class execution."""
1129    stage_name = STAGE_NAME_CLEAN_UP
1130    record = records.TestResultRecord(stage_name, self.TAG)
1131    record.test_begin()
1132    self.current_test_info = runtime_test_info.RuntimeTestInfo(
1133        stage_name, self.log_path, record
1134    )
1135    expects.recorder.reset_internal_states(record)
1136    with self._log_test_stage(stage_name):
1137      # Write controller info and summary to summary file.
1138      self._record_controller_info()
1139      self._controller_manager.unregister_controllers()
1140      if expects.recorder.has_error:
1141        record.test_error()
1142        record.update_record()
1143        self.results.add_class_error(record)
1144        self.summary_writer.dump(
1145            record.to_dict(), records.TestSummaryEntryType.RECORD
1146        )
1147