1# Copyright 2015-2017 Espressif Systems (Shanghai) PTE LTD
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
15"""
16Processing case config files.
17This is mainly designed for CI, we need to auto create and assign test jobs.
18
19Template Config File::
20
21    TestConfig:
22      app:
23        package: ttfw_idf
24        class: Example
25      dut:
26        path:
27        class:
28      config_file: /somewhere/config_file_for_runner
29      test_name: CI_test_job_1
30
31    Filter:
32      chip: ESP32
33      env_tag: default
34
35    CaseConfig:
36      - name: test_examples_protocol_https_request
37        # optional
38        extra_data: some extra data passed to case with kwarg extra_data
39        overwrite:  # overwrite test configs
40          app:
41            package: ttfw_idf
42            class: Example
43      - name: xxx
44"""
45import importlib
46
47import yaml
48
49try:
50    from yaml import CLoader as Loader
51except ImportError:
52    from yaml import Loader as Loader
53
54from . import TestCase
55
56
57def _convert_to_lower_case_bytes(item):
58    """
59    bot filter is always lower case string.
60    this function will convert to all string to lower case.
61    Note: Unicode strings are converted to bytes.
62    """
63    if isinstance(item, (tuple, list)):
64        output = [_convert_to_lower_case_bytes(v) for v in item]
65    elif isinstance(item, type(b'')):
66        output = item.lower()
67    elif isinstance(item, type(u'')):
68        output = item.encode().lower()
69    else:
70        output = item
71    return output
72
73
74def _filter_one_case(test_method, case_filter):
75    """ Apply filter for one case (the filter logic is the same as described in ``filter_test_cases``) """
76    filter_result = True
77    # filter keys are lower case. Do map lower case keys with original keys.
78    key_mapping = {x.lower(): x for x in test_method.case_info.keys()}
79
80    for orig_key in case_filter:
81        key = key_mapping[orig_key]
82        if key in test_method.case_info:
83            # the filter key is both in case and filter
84            # we need to check if they match
85            filter_item = _convert_to_lower_case_bytes(case_filter[orig_key])
86            accepted_item = _convert_to_lower_case_bytes(test_method.case_info[key])
87
88            if isinstance(filter_item, (tuple, list)) \
89                    and isinstance(accepted_item, (tuple, list)):
90                # both list/tuple, check if they have common item
91                filter_result = True if set(filter_item) & set(accepted_item) else False
92            elif isinstance(filter_item, (tuple, list)):
93                # filter item list/tuple, check if case accepted value in filter item list/tuple
94                filter_result = True if accepted_item in filter_item else False
95            elif isinstance(accepted_item, (tuple, list)):
96                # accepted item list/tuple, check if case filter value is in accept item list/tuple
97                filter_result = True if filter_item in accepted_item else False
98            else:
99                if type(filter_item) != type(accepted_item):
100                    # This will catch silent ignores of test cases when Unicode and bytes are compared
101                    raise AssertionError(filter_item, '!=', accepted_item)
102                # both string/int, just do string compare
103                filter_result = (filter_item == accepted_item)
104        else:
105            # key in filter only, which means the case supports all values for this filter key, match succeed
106            pass
107        if not filter_result:
108            # match failed
109            break
110    return filter_result
111
112
113def filter_test_cases(test_methods, case_filter):
114    """
115    filter test case. filter logic:
116
117        1. if filter key both in case attribute and filter:
118            * if both value is string/int, then directly compare
119            * if one is list/tuple, the other one is string/int, then check if string/int is in list/tuple
120            * if both are list/tuple, then check if they have common item
121        2. if only case attribute or filter have the key, filter succeed
122        3. will do case insensitive compare for string
123
124    for example, the following are match succeed scenarios
125    (the rule is symmetric, result is same if exchange values for user filter and case attribute):
126
127        * user case filter is ``chip: ["esp32", "esp32c"]``, case doesn't have ``chip`` attribute
128        * user case filter is ``chip: ["esp32", "esp32c"]``, case attribute is ``chip: "esp32"``
129        * user case filter is ``chip: "esp32"``, case attribute is ``chip: "esp32"``
130
131    :param test_methods: a list of test methods functions
132    :param case_filter: case filter
133    :return: filtered test methods
134    """
135    filtered_test_methods = []
136    for test_method in test_methods:
137        if _filter_one_case(test_method, case_filter):
138            filtered_test_methods.append(test_method)
139    return filtered_test_methods
140
141
142class Parser(object):
143    DEFAULT_CONFIG = {
144        'TestConfig': dict(),
145        'Filter': dict(),
146        'CaseConfig': [{'extra_data': None}],
147    }
148
149    @classmethod
150    def parse_config_file(cls, config_file):
151        """
152        parse from config file and then update to default config.
153
154        :param config_file: config file path
155        :return: configs
156        """
157        configs = cls.DEFAULT_CONFIG.copy()
158        if config_file:
159            with open(config_file, 'r') as f:
160                configs.update(yaml.load(f, Loader=Loader))
161        return configs
162
163    @classmethod
164    def handle_overwrite_args(cls, overwrite):
165        """
166        handle overwrite configs. import module from path and then get the required class.
167
168        :param overwrite: overwrite args
169        :return: dict of (original key: class)
170        """
171        output = dict()
172        for key in overwrite:
173            module = importlib.import_module(overwrite[key]['package'])
174            output[key] = module.__getattribute__(overwrite[key]['class'])
175        return output
176
177    @classmethod
178    def apply_config(cls, test_methods, config_file):
179        """
180        apply config for test methods
181
182        :param test_methods: a list of test methods functions
183        :param config_file: case filter file
184        :return: filtered cases
185        """
186        configs = cls.parse_config_file(config_file)
187        test_case_list = []
188        for _config in configs['CaseConfig']:
189            _filter = configs['Filter'].copy()
190            _overwrite = cls.handle_overwrite_args(_config.pop('overwrite', dict()))
191            _extra_data = _config.pop('extra_data', None)
192            _filter.update(_config)
193
194            # Try get target from yml
195            try:
196                _target = _filter['target']
197            except KeyError:
198                _target = None
199            else:
200                _overwrite.update({'target': _target})
201
202            for test_method in test_methods:
203                if _filter_one_case(test_method, _filter):
204                    try:
205                        dut_dict = test_method.case_info['dut_dict']
206                    except (AttributeError, KeyError):
207                        dut_dict = None
208
209                    if dut_dict and _target:
210                        dut = test_method.case_info.get('dut')
211                        if _target.upper() in dut_dict:
212                            if dut and dut in dut_dict.values():  # don't overwrite special cases
213                                _overwrite.update({'dut': dut_dict[_target.upper()]})
214                        else:
215                            raise ValueError('target {} is not in the specified dut_dict'.format(_target))
216                    test_case_list.append(TestCase.TestCase(test_method, _extra_data, **_overwrite))
217        return test_case_list
218
219
220class Generator(object):
221    """ Case config file generator """
222
223    def __init__(self):
224        self.default_config = {
225            'TestConfig': dict(),
226            'Filter': dict(),
227        }
228
229    def set_default_configs(self, test_config, case_filter):
230        """
231        :param test_config: "TestConfig" value
232        :param case_filter: "Filter" value
233        :return: None
234        """
235        self.default_config = {'TestConfig': test_config, 'Filter': case_filter}
236
237    def generate_config(self, case_configs, output_file):
238        """
239        :param case_configs: "CaseConfig" value
240        :param output_file: output file path
241        :return: None
242        """
243        config = self.default_config.copy()
244        config.update({'CaseConfig': case_configs})
245        with open(output_file, 'w') as f:
246            yaml.dump(config, f)
247