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
15from builtins import str
16
17import copy
18import io
19import pprint
20import os
21import yaml
22
23from mobly import keys
24from mobly import utils
25
26# An environment variable defining the base location for Mobly logs.
27ENV_MOBLY_LOGPATH = 'MOBLY_LOGPATH'
28_DEFAULT_LOG_PATH = '/tmp/logs/mobly/'
29
30
31class MoblyConfigError(Exception):
32  """Raised when there is a problem in test configuration file."""
33
34
35def _validate_test_config(test_config):
36  """Validates the raw configuration loaded from the config file.
37
38  Making sure the required key 'TestBeds' is present.
39  """
40  required_key = keys.Config.key_testbed.value
41  if required_key not in test_config:
42    raise MoblyConfigError(
43        'Required key %s missing in test config.' % required_key
44    )
45
46
47def _validate_testbed_name(name):
48  """Validates the name of a test bed.
49
50  Since test bed names are used as part of the test run id, it needs to meet
51  certain requirements.
52
53  Args:
54    name: The test bed's name specified in config file.
55
56  Raises:
57    MoblyConfigError: The name does not meet any criteria.
58  """
59  if not name:
60    raise MoblyConfigError("Test bed names can't be empty.")
61  name = str(name)
62  for char in name:
63    if char not in utils.valid_filename_chars:
64      raise MoblyConfigError(
65          'Char "%s" is not allowed in test bed names.' % char
66      )
67
68
69def _validate_testbed_configs(testbed_configs):
70  """Validates the testbed configurations.
71
72  Args:
73    testbed_configs: A list of testbed configuration dicts.
74
75  Raises:
76    MoblyConfigError: Some parts of the configuration is invalid.
77  """
78  seen_names = set()
79  # Cross checks testbed configs for resource conflicts.
80  for config in testbed_configs:
81    # Check for conflicts between multiple concurrent testbed configs.
82    # No need to call it if there's only one testbed config.
83    name = config[keys.Config.key_testbed_name.value]
84    _validate_testbed_name(name)
85    # Test bed names should be unique.
86    if name in seen_names:
87      raise MoblyConfigError('Duplicate testbed name %s found.' % name)
88    seen_names.add(name)
89
90
91def load_test_config_file(test_config_path, tb_filters=None):
92  """Processes the test configuration file provied by user.
93
94  Loads the configuration file into a dict, unpacks each testbed
95  config into its own dict, and validate the configuration in the
96  process.
97
98  Args:
99    test_config_path: Path to the test configuration file.
100    tb_filters: A subset of test bed names to be pulled from the config
101      file. If None, then all test beds will be selected.
102
103  Returns:
104    A list of test configuration dicts to be passed to
105    test_runner.TestRunner.
106  """
107  configs = _load_config_file(test_config_path)
108  if tb_filters:
109    tbs = []
110    for tb in configs[keys.Config.key_testbed.value]:
111      if tb[keys.Config.key_testbed_name.value] in tb_filters:
112        tbs.append(tb)
113    if len(tbs) != len(tb_filters):
114      raise MoblyConfigError(
115          'Expect to find %d test bed configs, found %d. Check if'
116          ' you have the correct test bed names.' % (len(tb_filters), len(tbs))
117      )
118    configs[keys.Config.key_testbed.value] = tbs
119  mobly_params = configs.get(keys.Config.key_mobly_params.value, {})
120  # Decide log path.
121  log_path = mobly_params.get(keys.Config.key_log_path.value, _DEFAULT_LOG_PATH)
122  if ENV_MOBLY_LOGPATH in os.environ:
123    log_path = os.environ[ENV_MOBLY_LOGPATH]
124  log_path = utils.abs_path(log_path)
125  # Validate configs
126  _validate_test_config(configs)
127  _validate_testbed_configs(configs[keys.Config.key_testbed.value])
128  # Transform config dict from user-facing key mapping to internal config object.
129  test_configs = []
130  for original_bed_config in configs[keys.Config.key_testbed.value]:
131    test_run_config = TestRunConfig()
132    test_run_config.testbed_name = original_bed_config[
133        keys.Config.key_testbed_name.value
134    ]
135    # Deprecated, use testbed_name
136    test_run_config.test_bed_name = test_run_config.testbed_name
137    test_run_config.log_path = log_path
138    test_run_config.controller_configs = original_bed_config.get(
139        keys.Config.key_testbed_controllers.value, {}
140    )
141    test_run_config.user_params = original_bed_config.get(
142        keys.Config.key_testbed_test_params.value, {}
143    )
144    test_configs.append(test_run_config)
145  return test_configs
146
147
148def _load_config_file(path):
149  """Loads a test config file.
150
151  The test config file has to be in YAML format.
152
153  Args:
154    path: A string that is the full path to the config file, including the
155      file name.
156
157  Returns:
158    A dict that represents info in the config file.
159  """
160  with io.open(utils.abs_path(path), 'r', encoding='utf-8') as f:
161    conf = yaml.safe_load(f)
162    return conf
163
164
165class TestRunConfig:
166  """The data class that holds all the information needed for a test run.
167
168  Attributes:
169    log_path: string, specifies the root directory for all logs written by
170      a test run.
171    test_bed_name: [Deprecated, use 'testbed_name' instead]
172      string, the name of the test bed used by a test run.
173    testbed_name: string, the name of the test bed used by a test run.
174    controller_configs: dict, configs used for instantiating controller
175      objects.
176    user_params: dict, all the parameters to be consumed by the test logic.
177    summary_writer: records.TestSummaryWriter, used to write elements to
178      the test result summary file.
179    test_class_name_suffix: string, suffix to append to the class name for
180        reporting. This is used for differentiating the same class
181        executed with different parameters in a suite.
182  """
183
184  def __init__(self):
185    self.log_path = _DEFAULT_LOG_PATH
186    # Deprecated, use 'testbed_name'
187    self.test_bed_name = None
188    self.testbed_name = None
189    self.controller_configs = {}
190    self.user_params = {}
191    self.summary_writer = None
192    self.test_class_name_suffix = None
193
194  def copy(self):
195    """Returns a deep copy of the current config."""
196    return copy.deepcopy(self)
197
198  def __str__(self):
199    content = dict(self.__dict__)
200    content.pop('summary_writer')
201    return pprint.pformat(content)
202