1import copy
2import glob
3import os
4import os.path
5import re
6import shutil
7
8
9def action_extensions(base_actions, project_path=os.getcwd()):
10    """ Describes extensions for unit tests. This function expects that actions "all" and "reconfigure" """
11
12    PROJECT_NAME = 'unit-test-app'
13
14    # List of unit-test-app configurations.
15    # Each file in configs/ directory defines a configuration. The format is the
16    # same as sdkconfig file. Configuration is applied on top of sdkconfig.defaults
17    # file from the project directory
18    CONFIG_NAMES = os.listdir(os.path.join(project_path, 'configs'))
19
20    # Build (intermediate) and output (artifact) directories
21    BUILDS_DIR = os.path.join(project_path, 'builds')
22    BINARIES_DIR = os.path.join(project_path, 'output')
23
24    def parse_file_to_dict(path, regex):
25        """
26        Parse the config file at 'path'
27
28        Returns a dict of name:value.
29        """
30        compiled_regex = re.compile(regex)
31        result = {}
32        with open(path) as f:
33            for line in f:
34                m = compiled_regex.match(line)
35                if m:
36                    result[m.group(1)] = m.group(2)
37        return result
38
39    def parse_config(path):
40        """
41        Expected format with default regex is "key=value"
42        """
43
44        return parse_file_to_dict(path, r'^([^=]+)=(.+)$')
45
46    def ut_apply_config(ut_apply_config_name, ctx, args):
47        config_name = re.match(r'ut-apply-config-(.*)', ut_apply_config_name).group(1)
48        # Make sure that define_cache_entry is list
49        args.define_cache_entry = list(args.define_cache_entry)
50        new_cache_values = {}
51        sdkconfig_set = list(filter(lambda s: 'SDKCONFIG=' in s, args.define_cache_entry))
52        sdkconfig_path = os.path.join(args.project_dir, 'sdkconfig')
53
54        if sdkconfig_set:
55            sdkconfig_path = sdkconfig_set[-1].split('=')[1]
56            sdkconfig_path = os.path.abspath(sdkconfig_path)
57
58        try:
59            os.remove(sdkconfig_path)
60        except OSError:
61            pass
62
63        if config_name in CONFIG_NAMES:
64            # Parse the sdkconfig for components to be included/excluded and tests to be run
65            config_path = os.path.join(project_path, 'configs', config_name)
66            config = parse_config(config_path)
67
68            target = config.get('CONFIG_IDF_TARGET', 'esp32').strip("'").strip('"')
69
70            print('Reconfigure: config %s, target %s' % (config_name, target))
71
72            # Clean up and set idf-target
73            base_actions['actions']['fullclean']['callback']('fullclean', ctx, args)
74
75            new_cache_values['EXCLUDE_COMPONENTS'] = config.get('EXCLUDE_COMPONENTS', "''")
76            new_cache_values['TEST_EXCLUDE_COMPONENTS'] = config.get('TEST_EXCLUDE_COMPONENTS', "''")
77            new_cache_values['TEST_COMPONENTS'] = config.get('TEST_COMPONENTS', "''")
78            new_cache_values['TESTS_ALL'] = int(new_cache_values['TEST_COMPONENTS'] == "''")
79            new_cache_values['IDF_TARGET'] = target
80            new_cache_values['SDKCONFIG_DEFAULTS'] = ';'.join([os.path.join(project_path, 'sdkconfig.defaults'), config_path])
81
82            args.define_cache_entry.extend(['%s=%s' % (k, v) for k, v in new_cache_values.items()])
83
84            reconfigure = base_actions['actions']['reconfigure']['callback']
85            reconfigure(None, ctx, args)
86
87    # This target builds the configuration. It does not currently track dependencies,
88    # but is good enough for CI builds if used together with clean-all-configs.
89    # For local builds, use 'apply-config-NAME' target and then use normal 'all'
90    # and 'flash' targets.
91    def ut_build(ut_build_name, ctx, args):
92        # Create a copy of the passed arguments to prevent arg modifications to accrue if
93        # all configs are being built
94        build_args = copy.copy(args)
95
96        config_name = re.match(r'ut-build-(.*)', ut_build_name).group(1)
97
98        if config_name in CONFIG_NAMES:
99            build_args.build_dir = os.path.join(BUILDS_DIR, config_name)
100
101            src = os.path.join(BUILDS_DIR, config_name)
102            dest = os.path.join(BINARIES_DIR, config_name)
103
104            try:
105                os.makedirs(dest)
106            except OSError:
107                pass
108
109            # Build, tweaking paths to sdkconfig and sdkconfig.defaults
110            ut_apply_config('ut-apply-config-' + config_name, ctx, build_args)
111
112            build_target = base_actions['actions']['all']['callback']
113
114            build_target('all', ctx, build_args)
115
116            # Copy artifacts to the output directory
117            shutil.copyfile(
118                os.path.join(build_args.project_dir, 'sdkconfig'),
119                os.path.join(dest, 'sdkconfig'),
120            )
121
122            binaries = [PROJECT_NAME + x for x in ['.elf', '.bin', '.map']]
123
124            for binary in binaries:
125                shutil.copyfile(os.path.join(src, binary), os.path.join(dest, binary))
126
127            try:
128                os.mkdir(os.path.join(dest, 'bootloader'))
129            except OSError:
130                pass
131
132            shutil.copyfile(
133                os.path.join(src, 'bootloader', 'bootloader.bin'),
134                os.path.join(dest, 'bootloader', 'bootloader.bin'),
135            )
136
137            for partition_table in glob.glob(os.path.join(src, 'partition_table', 'partition-table*.bin')):
138                try:
139                    os.mkdir(os.path.join(dest, 'partition_table'))
140                except OSError:
141                    pass
142                shutil.copyfile(
143                    partition_table,
144                    os.path.join(dest, 'partition_table', os.path.basename(partition_table)),
145                )
146
147            shutil.copyfile(
148                os.path.join(src, 'flasher_args.json'),
149                os.path.join(dest, 'flasher_args.json'),
150            )
151
152            binaries = glob.glob(os.path.join(src, '*.bin'))
153            binaries = [os.path.basename(s) for s in binaries]
154
155            for binary in binaries:
156                shutil.copyfile(os.path.join(src, binary), os.path.join(dest, binary))
157
158    def ut_clean(ut_clean_name, ctx, args):
159        config_name = re.match(r'ut-clean-(.*)', ut_clean_name).group(1)
160        if config_name in CONFIG_NAMES:
161            shutil.rmtree(os.path.join(BUILDS_DIR, config_name), ignore_errors=True)
162            shutil.rmtree(os.path.join(BINARIES_DIR, config_name), ignore_errors=True)
163
164    def test_component_callback(ctx, global_args, tasks):
165        """ Convert the values passed to the -T and -E parameter to corresponding cache entry definitions TESTS_ALL and TEST_COMPONENTS """
166        test_components = global_args.test_components
167        test_exclude_components = global_args.test_exclude_components
168
169        cache_entries = {}
170
171        if test_components:
172            if 'all' in test_components:
173                cache_entries['TESTS_ALL'] = 1
174                cache_entries['TEST_COMPONENTS'] = "''"
175            else:
176                cache_entries['TESTS_ALL'] = 0
177                cache_entries['TEST_COMPONENTS'] = ' '.join(test_components)
178
179        if test_exclude_components:
180            cache_entries['TEST_EXCLUDE_COMPONENTS'] = ' '.join(test_exclude_components)
181
182        if cache_entries:
183            global_args.define_cache_entry = list(global_args.define_cache_entry)
184            global_args.define_cache_entry.extend(['%s=%s' % (k, v) for k, v in cache_entries.items()])
185
186    # Add global options
187    extensions = {
188        'global_options': [{
189            'names': ['-T', '--test-components'],
190            'help': 'Specify the components to test.',
191            'scope': 'shared',
192            'multiple': True,
193        }, {
194            'names': ['-E', '--test-exclude-components'],
195            'help': 'Specify the components to exclude from testing.',
196            'scope': 'shared',
197            'multiple': True,
198        }],
199        'global_action_callbacks': [test_component_callback],
200        'actions': {},
201    }
202
203    # This generates per-config targets (clean, build, apply-config).
204    build_all_config_deps = []
205    clean_all_config_deps = []
206
207    for config in CONFIG_NAMES:
208        config_build_action_name = 'ut-build-' + config
209        config_clean_action_name = 'ut-clean-' + config
210        config_apply_config_action_name = 'ut-apply-config-' + config
211
212        extensions['actions'][config_build_action_name] = {
213            'callback':
214            ut_build,
215            'help':
216            'Build unit-test-app with configuration provided in configs/NAME. ' +
217            'Build directory will be builds/%s/, ' % config_build_action_name +
218            'output binaries will be under output/%s/' % config_build_action_name,
219        }
220
221        extensions['actions'][config_clean_action_name] = {
222            'callback': ut_clean,
223            'help': 'Remove build and output directories for configuration %s.' % config_clean_action_name,
224        }
225
226        extensions['actions'][config_apply_config_action_name] = {
227            'callback':
228            ut_apply_config,
229            'help':
230            'Generates configuration based on configs/%s in sdkconfig file.' % config_apply_config_action_name +
231            'After this, normal all/flash targets can be used. Useful for development/debugging.',
232        }
233
234        build_all_config_deps.append(config_build_action_name)
235        clean_all_config_deps.append(config_clean_action_name)
236
237    extensions['actions']['ut-build-all-configs'] = {
238        'callback': ut_build,
239        'help': 'Build all configurations defined in configs/ directory.',
240        'dependencies': build_all_config_deps,
241    }
242
243    extensions['actions']['ut-clean-all-configs'] = {
244        'callback': ut_clean,
245        'help': 'Remove build and output directories for all configurations defined in configs/ directory.',
246        'dependencies': clean_all_config_deps,
247    }
248
249    return extensions
250