blob: 72732b565fc8dfe0c4dc345f81f1eabe32f7d256 [file] [log] [blame]
# Copyright 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Runner for Mobly test suites.
These is just example code to help users run a collection of Mobly test
classes. Users can use it as is or customize it based on their requirements.
There are two ways to use this runner.
1. Call suite_runner.run_suite() with one or more individual test classes. This
is for users who just need to execute a collection of test classes without any
additional steps.
.. code-block:: python
from mobly import suite_runner
from my.test.lib import foo_test
from my.test.lib import bar_test
...
if __name__ == '__main__':
suite_runner.run_suite(foo_test.FooTest, bar_test.BarTest)
2. Create a subclass of base_suite.BaseSuite and add the individual test
classes. Using the BaseSuite class allows users to define their own setup
and teardown steps on the suite level as well as custom config for each test
class.
.. code-block:: python
from mobly import base_suite
from mobly import suite_runner
from my.path import MyFooTest
from my.path import MyBarTest
class MySuite(base_suite.BaseSuite):
def setup_suite(self, config):
# Add a class with default config.
self.add_test_class(MyFooTest)
# Add a class with test selection.
self.add_test_class(MyBarTest,
tests=['test_a', 'test_b'])
# Add the same class again with a custom config and suffix.
my_config = some_config_logic(config)
self.add_test_class(MyBarTest,
config=my_config,
name_suffix='WithCustomConfig')
if __name__ == '__main__':
suite_runner.run_suite_class()
"""
import argparse
import collections
import inspect
import logging
import sys
from mobly import base_test
from mobly import base_suite
from mobly import config_parser
from mobly import signals
from mobly import test_runner
class Error(Exception):
pass
def _parse_cli_args(argv):
"""Parses cli args that are consumed by Mobly.
Args:
argv: A list that is then parsed as cli args. If None, defaults to cli
input.
Returns:
Namespace containing the parsed args.
"""
parser = argparse.ArgumentParser(description='Mobly Suite Executable.')
parser.add_argument('-c',
'--config',
type=str,
required=True,
metavar='<PATH>',
help='Path to the test configuration file.')
parser.add_argument(
'--tests',
'--test_case',
nargs='+',
type=str,
metavar='[ClassA[.test_a] ClassB[.test_b] ...]',
help='A list of test classes and optional tests to execute.')
if not argv:
argv = sys.argv[1:]
return parser.parse_args(argv)
def _find_suite_class():
"""Finds the test suite class in the current module.
Walk through module members and find the subclass of BaseSuite. Only
one subclass is allowed in a module.
Returns:
The test suite class in the test module.
"""
test_suites = []
main_module_members = sys.modules['__main__']
for _, module_member in main_module_members.__dict__.items():
if inspect.isclass(module_member):
if issubclass(module_member, base_suite.BaseSuite):
test_suites.append(module_member)
if len(test_suites) != 1:
logging.error('Expected 1 test class per file, found %s.',
[t.__name__ for t in test_suites])
sys.exit(1)
return test_suites[0]
def run_suite_class(argv=None):
"""Executes tests in the test suite.
Args:
argv: A list that is then parsed as CLI args. If None, defaults to sys.argv.
"""
cli_args = _parse_cli_args(argv)
test_configs = config_parser.load_test_config_file(cli_args.config)
config_count = len(test_configs)
if config_count != 1:
logging.error('Expect exactly one test config, found %d', config_count)
config = test_configs[0]
runner = test_runner.TestRunner(
log_dir=config.log_path, testbed_name=config.testbed_name)
suite_class = _find_suite_class()
suite = suite_class(runner, config)
ok = False
with runner.mobly_logger():
try:
suite.setup_suite(config.copy())
try:
runner.run()
ok = runner.results.is_all_pass
print(ok)
except signals.TestAbortAll:
pass
finally:
suite.teardown_suite()
if not ok:
sys.exit(1)
def run_suite(test_classes, argv=None):
"""Executes multiple test classes as a suite.
This is the default entry point for running a test suite script file
directly.
Args:
test_classes: List of python classes containing Mobly tests.
argv: A list that is then parsed as cli args. If None, defaults to cli
input.
"""
args = _parse_cli_args(argv)
# Load test config file.
test_configs = config_parser.load_test_config_file(args.config)
# Check the classes that were passed in
for test_class in test_classes:
if not issubclass(test_class, base_test.BaseTestClass):
logging.error(
'Test class %s does not extend '
'mobly.base_test.BaseTestClass', test_class)
sys.exit(1)
# Find the full list of tests to execute
selected_tests = compute_selected_tests(test_classes, args.tests)
# Execute the suite
ok = True
for config in test_configs:
runner = test_runner.TestRunner(config.log_path, config.testbed_name)
with runner.mobly_logger():
for (test_class, tests) in selected_tests.items():
runner.add_test_class(config, test_class, tests)
try:
runner.run()
ok = runner.results.is_all_pass and ok
except signals.TestAbortAll:
pass
except Exception:
logging.exception('Exception when executing %s.', config.testbed_name)
ok = False
if not ok:
sys.exit(1)
def compute_selected_tests(test_classes, selected_tests):
"""Computes tests to run for each class from selector strings.
This function transforms a list of selector strings (such as FooTest or
FooTest.test_method_a) to a dict where keys are test_name classes, and
values are lists of selected tests in those classes. None means all tests in
that class are selected.
Args:
test_classes: list of strings, names of all the classes that are part
of a suite.
selected_tests: list of strings, list of tests to execute. If empty,
all classes `test_classes` are selected. E.g.
.. code-block:: python
[
'FooTest',
'BarTest',
'BazTest.test_method_a',
'BazTest.test_method_b'
]
Returns:
dict: Identifiers for TestRunner. Keys are test class names; valures
are lists of test names within class. E.g. the example in
`selected_tests` would translate to:
.. code-block:: python
{
FooTest: None,
BarTest: None,
BazTest: ['test_method_a', 'test_method_b']
}
This dict is easy to consume for `TestRunner`.
"""
class_to_tests = collections.OrderedDict()
if not selected_tests:
# No selection is needed; simply run all tests in all classes.
for test_class in test_classes:
class_to_tests[test_class] = None
return class_to_tests
# The user is selecting some tests to run. Parse the selectors.
# Dict from test_name class name to list of tests to execute (or None for all
# tests).
test_class_name_to_tests = collections.OrderedDict()
for test_name in selected_tests:
if '.' in test_name: # Has a test method
(test_class_name, test_name) = test_name.split('.')
if test_class_name not in test_class_name_to_tests:
# Never seen this class before
test_class_name_to_tests[test_class_name] = [test_name]
elif test_class_name_to_tests[test_class_name] is None:
# Already running all tests in this class, so ignore this extra
# test.
pass
else:
test_class_name_to_tests[test_class_name].append(test_name)
else: # No test method; run all tests in this class.
test_class_name_to_tests[test_name] = None
# Now transform class names to class objects.
# Dict from test_name class name to instance.
class_name_to_class = {cls.__name__: cls for cls in test_classes}
for test_class_name, tests in test_class_name_to_tests.items():
test_class = class_name_to_class.get(test_class_name)
if not test_class:
raise Error('Unknown test_name class %s' % test_class_name)
class_to_tests[test_class] = tests
return class_to_tests