• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3#   Copyright 2018 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an "AS IS" BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16import logging
17import os
18
19from acts import utils
20from acts.config import entries
21
22from acts.config.config_sources.cli_config_source import CliConfigSource
23from acts.config.config_sources.env_config_source import EnvConfigSource
24from acts.config.config_sources.file_config_source import FileConfigSource
25from acts.config.config_wrapper import ConfigWrapper
26from acts.keys import Config
27
28
29class ActsConfigError(Exception):
30    """Raised when there is a problem in test configuration file."""
31
32
33class ConfigGenerator(object):
34    """A class for generating the master configuration for running tests."""
35
36    def __init__(self):
37        """Initializes the ConfigGenerator."""
38        self._master_config = {}
39
40    def _get_test_config(self, testbed_name):
41        """Returns a test config for a given testbed."""
42        test_config = dict(self._master_config)
43        testbed_data = (
44            self._master_config[Config.key_testbed.value][testbed_name])
45
46        test_config[Config.key_testbed.value] = testbed_data
47        # Keys in each test bed config will be copied to a level up to be
48        # picked up for user_params. If the key already exists in the upper
49        # level, the local one defined in test bed config overwrites the
50        # general one.
51        test_config.update(testbed_data)
52        return ConfigWrapper(test_config)
53
54    def generate_configs(self):
55        """Returns the configuration to be used for the tests and framework."""
56        config_entries = entries.config_entries
57        for config_src in self._get_config_sources():
58            for key, value in config_src.gather_configs(config_entries).items():
59                if key not in self._master_config:
60                    self._master_config[key] = value
61
62        self._post_process_configs()
63
64        generated_testbeds = []
65        for name in self._master_config.get(
66                Config.key_testbeds_under_test.value,
67                self._master_config[Config.key_testbed.value].keys()):
68            generated_testbeds.append(self._get_test_config(name))
69        return generated_testbeds
70
71    def _get_config_sources(self):
72        """Returns the sources in the order of most-to-least significant.
73
74        Values from ConfigSources returned earlier should not be overwritten by
75        the values obtained by later sources.
76        """
77        yield CliConfigSource()
78        yield FileConfigSource(
79            self._master_config[Config.key_config_full_path.value])
80        yield EnvConfigSource()
81
82    def _post_process_configs(self):
83        """Does post processing on the configs.
84
85        This function will handle obtaining additional values from the configs,
86        well as handle the testbed config creation.
87        """
88        # Sets the config directory.
89        # TODO(b/29836695): Remove after the key has been deprecated.
90        config_dir = os.path.dirname(
91            self._master_config[Config.key_config_full_path.value])
92        self._master_config[Config.key_config_path] = config_dir
93
94        # Normalizes the "testbed" field to be a dictionary if not already.
95        if type(self._master_config[Config.key_testbed.value]) is list:
96            self._master_config[Config.key_testbed.value] = (
97                utils.list_of_dict_to_dict_of_dict(
98                    self._master_config[Config.key_testbed.value],
99                    Config.key_testbed_name.value))
100
101        # For backwards compatibility, makes sure the name of each testbed name
102        # is available to the testbed data.
103        for name, testbed in (
104                   self._master_config[Config.key_testbed.value].items()):
105            if Config.key_testbed_name.value not in testbed:
106                testbed[Config.key_testbed_name.value] = name
107
108        # Validates the given configs.
109        _validate_test_config(self._master_config)
110        _validate_testbed_configs(self._master_config[Config.key_testbed.value],
111                                  config_dir)
112
113
114def _validate_test_config(test_config):
115    """Validates the raw configuration loaded from the config file.
116
117    Makes sure all the required fields exist.
118    """
119    for k in Config.reserved_keys.value:
120        if k not in test_config:
121            raise ActsConfigError(
122                'Required key "%s" is missing from the test config.' % k)
123
124
125def _validate_testbed_configs(testbed_configs, config_path):
126    """Validates the testbed configurations.
127
128    Args:
129        testbed_configs: A list of testbed configuration json objects.
130        config_path : The path to the config file, which can be used to
131                      generate absolute paths from relative paths in configs.
132
133    Raises:
134        If any part of the configuration is invalid, ActsConfigError is raised.
135    """
136    for name, config in testbed_configs.items():
137        _validate_testbed_name(name)
138        # TODO(b/78189048): Remove this after deprecating relative paths.
139        _update_file_paths(config, config_path)
140
141
142def _update_file_paths(config, config_path):
143    """ Checks if the path entries are valid.
144
145    If the file path is invalid, assume it is a relative path and append
146    that to the config file path.
147
148    Args:
149        config : the config object to verify.
150        config_path : The path to the config file, which can be used to
151                      generate absolute paths from relative paths in configs.
152
153    Raises:
154        If the file path is invalid, ActsConfigError is raised.
155    """
156    # Check the file_path_keys and update if it is a relative path.
157    for file_path_key in Config.file_path_keys.value:
158        if file_path_key in config:
159            config_file = config[file_path_key]
160            if type(config_file) is str:
161                if os.path.isabs(config_file):
162                    continue
163                logging.warning(
164                    'Relative paths for key "%s" will be deprecated shortly. '
165                    'Please update your configuration to use an absolute path.',
166                    file_path_key
167                )
168                config_file = os.path.join(config_path, config_file)
169                if not os.path.isfile(config_file):
170                    raise ActsConfigError('Unable to load config %s from test '
171                                          'config file.', config_file)
172                config[file_path_key] = config_file
173
174
175def _validate_testbed_name(name):
176    """Validates the name of a test bed.
177
178    Since test bed names are used as part of the test run id, it needs to meet
179    certain requirements.
180
181    Args:
182        name: The test bed's name specified in config file.
183
184    Raises:
185        If the name does not meet any criteria, ActsConfigError is raised.
186    """
187    if not name:
188        raise ActsConfigError('Testbed names cannot be empty.')
189    if not isinstance(name, str):
190        raise ActsConfigError('Testbed names have to be string.')
191    invalid_chars = set(name) - set(utils.valid_filename_chars)
192    if invalid_chars:
193        raise ActsConfigError(
194            'Testbed "%s" has following disallowed characters: %s' %
195            (name, set(name) - set(utils.valid_filename_chars)))
196