1#!/usr/bin/env python3
2
3import argparse
4import errno
5import shutil
6import subprocess
7import sys
8import os
9import platform
10from itertools import chain
11from pathlib import Path
12
13lvgl_test_dir = os.path.dirname(os.path.realpath(__file__))
14lvgl_script_path = os.path.join(lvgl_test_dir, "../scripts")
15sys.path.append(lvgl_script_path)
16
17wayland_dir = os.path.join(lvgl_test_dir, "wayland_protocols")
18wayland_protocols_dir = os.path.realpath("/usr/share/wayland-protocols")
19
20from LVGLImage import LVGLImage, ColorFormat, CompressMethod
21
22# Key values must match variable names in CMakeLists.txt.
23build_only_options = {
24#    'OPTIONS_NORMAL_8BIT': 'Normal config, 8 bit color depth',
25    'OPTIONS_16BIT': 'Minimal config, 16 bit color depth',
26    'OPTIONS_24BIT': 'Normal config, 24 bit color depth',
27    'OPTIONS_FULL_32BIT': 'Full config, 32 bit color depth',
28    'OPTIONS_VG_LITE': 'VG-Lite simulator with full config, 32 bit color depth',
29}
30
31if platform.system() != 'Windows':
32    build_only_options['OPTIONS_SDL'] = 'SDL simulator with full config, 32 bit color depth'
33
34test_options = {
35    'OPTIONS_TEST_SYSHEAP': 'Test config, system heap, 32 bit color depth',
36    'OPTIONS_TEST_DEFHEAP': 'Test config, LVGL heap, 32 bit color depth',
37    'OPTIONS_TEST_VG_LITE': 'VG-Lite simulator with full config, 32 bit color depth',
38}
39
40
41def get_option_description(option_name):
42    if option_name in build_only_options:
43        return build_only_options[option_name]
44    return test_options[option_name]
45
46
47def delete_dir_ignore_missing(dir_path):
48    '''Recursively delete a directory and ignore if missing.'''
49    try:
50        shutil.rmtree(dir_path)
51    except FileNotFoundError:
52        pass
53
54
55def options_abbrev(options_name):
56    '''Return an abbreviated version of the option name.'''
57    prefix = 'OPTIONS_'
58    assert options_name.startswith(prefix)
59    return options_name[len(prefix):].lower()
60
61
62def get_base_build_dir(options_name):
63    '''Given the build options name, return the build directory name.
64
65    Does not return the full path to the directory - just the base name.'''
66    return 'build_%s' % options_abbrev(options_name)
67
68
69def get_build_dir(options_name):
70    '''Given the build options name, return the build directory name.
71
72    Returns absolute path to the build directory.'''
73    global lvgl_test_dir
74    return os.path.join(lvgl_test_dir, get_base_build_dir(options_name))
75
76def gen_wayland_protocols(clean):
77    '''Generates the xdg shell interface from wayland protocol definitions'''
78
79    if clean:
80        delete_dir_ignore_missing(wayland_dir)
81
82    if not os.path.isdir(wayland_dir):
83        os.mkdir(wayland_dir)
84        subprocess.check_call(['wayland-scanner',
85            'client-header',
86            os.path.join(wayland_protocols_dir, "stable/xdg-shell/xdg-shell.xml"),
87            os.path.join(wayland_dir, "wayland_xdg_shell.h.original"),
88        ])
89
90        subprocess.check_call(['wayland-scanner',
91            'private-code',
92            os.path.join(wayland_protocols_dir, "stable/xdg-shell/xdg-shell.xml"),
93            os.path.join(wayland_dir, "wayland_xdg_shell.c.original"),
94        ])
95
96        # Insert guards
97        with open(os.path.join(wayland_dir, "wayland_xdg_shell.h"), "w") as outfile:
98            subprocess.check_call(['sed','-e', "1i #if LV_BUILD_TEST", '-e', '$a #endif',
99                os.path.join(wayland_dir, "wayland_xdg_shell.h.original")], stdout=outfile)
100
101        with open(os.path.join(wayland_dir, "wayland_xdg_shell.c"), "w") as outfile:
102            subprocess.check_call(['sed','-e', "1i #if LV_BUILD_TEST", '-e', '$a #endif',
103                os.path.join(wayland_dir, "wayland_xdg_shell.c.original")], stdout=outfile)
104
105        subprocess.check_call(['rm', os.path.join(wayland_dir, "wayland_xdg_shell.c.original")])
106        subprocess.check_call(['rm', os.path.join(wayland_dir, "wayland_xdg_shell.h.original")])
107
108def build_tests(options_name, build_type, clean):
109    '''Build all tests for the specified options name.'''
110    global lvgl_test_dir
111
112    print()
113    print()
114    label = 'Building: %s: %s' % (options_abbrev(
115        options_name), get_option_description(options_name))
116    print('=' * len(label))
117    print(label)
118    print('=' * len(label))
119    print(flush=True)
120
121    build_dir = get_build_dir(options_name)
122    if clean:
123        delete_dir_ignore_missing(build_dir)
124
125    os.chdir(lvgl_test_dir)
126
127    if platform.system() != 'Windows':
128        gen_wayland_protocols(clean)
129
130    created_build_dir = False
131    if not os.path.isdir(build_dir):
132        os.mkdir(build_dir)
133        created_build_dir = True
134    os.chdir(build_dir)
135    if created_build_dir:
136        subprocess.check_call(['cmake', '-GNinja', '-DCMAKE_BUILD_TYPE=%s' % build_type,
137                               '-D%s=1' % options_name, '..'])
138    subprocess.check_call(['cmake', '--build', build_dir,
139                           '--parallel', str(os.cpu_count())])
140
141
142def run_tests(options_name, test_suite):
143    '''Run the tests for the given options name.'''
144
145    print()
146    print()
147    label = 'Running tests for %s' % options_abbrev(options_name)
148    print('=' * len(label))
149    print(label)
150    print('=' * len(label), flush=True)
151
152    os.chdir(get_build_dir(options_name))
153    args = [
154        'ctest',
155        '--timeout', '300',
156        '--parallel', str(os.cpu_count()),
157        '--output-on-failure',
158    ]
159    if test_suite is not None:
160        args.extend(["--tests-regex", test_suite])
161    subprocess.check_call(args)
162
163
164def generate_code_coverage_report():
165    '''Produce code coverage test reports for the test execution.'''
166    global lvgl_test_dir
167
168    print()
169    print()
170    label = 'Generating code coverage reports'
171    print('=' * len(label))
172    print(label)
173    print('=' * len(label))
174    print(flush=True)
175
176    os.chdir(lvgl_test_dir)
177    delete_dir_ignore_missing('report')
178    os.mkdir('report')
179    root_dir = os.pardir
180    html_report_file = 'report/index.html'
181    cmd = ['gcovr', '--root', root_dir, '--html-details', '--output',
182           html_report_file, '--xml', 'report/coverage.xml',
183           '-j', str(os.cpu_count()), '--print-summary',
184           '--html-title', 'LVGL Test Coverage', '--filter', r'../src/.*/lv_.*\.c']
185
186    subprocess.check_call(cmd)
187    print("Done: See %s" % html_report_file, flush=True)
188
189
190def generate_test_images():
191    invalids = (ColorFormat.UNKNOWN,ColorFormat.RAW,ColorFormat.RAW_ALPHA)
192    formats = [i for i in ColorFormat if i not in invalids]
193    png_path = os.path.join(lvgl_test_dir, "test_images/pngs")
194    pngs = list(Path(png_path).rglob("*.[pP][nN][gG]"))
195    print(f"png files: {pngs}")
196
197    align_options = [1, 64]
198
199    for align in align_options:
200        for compress in CompressMethod:
201            compress_name = compress.name if compress != CompressMethod.NONE else "UNCOMPRESSED"
202            outputs = os.path.join(lvgl_test_dir, f"test_images/stride_align{align}/{compress_name}/")
203            os.makedirs(outputs, exist_ok=True)
204            for fmt in formats:
205                for png in pngs:
206                    img = LVGLImage().from_png(png, cf=fmt, background=0xffffff)
207                    img.adjust_stride(align=16)
208                    output = os.path.join(outputs, f"{Path(png).stem}_{fmt.name}.bin")
209                    img.to_bin(output, compress=compress)
210                    output = os.path.join(outputs, f"{Path(png).stem}_{fmt.name}_{compress.name}_align{align}.c")
211                    img.to_c_array(output, compress=compress)
212                    print(f"converting {os.path.basename(png)}, format: {fmt.name}, compress: {compress_name}")
213
214
215if __name__ == "__main__":
216    epilog = '''This program builds and optionally runs the LVGL test programs.
217    There are two types of LVGL tests: "build", and "test". The build-only
218    tests, as their name suggests, only verify that the program successfully
219    compiles and links (with various build options). There are also a set of
220    tests that execute to verify correct LVGL library behavior.
221    '''
222    parser = argparse.ArgumentParser(
223        description='Build and/or run LVGL tests.', epilog=epilog)
224    parser.add_argument('--build-options', nargs=1,
225                        choices=list(chain(build_only_options, test_options)),
226                        help='''the build option name to build or run. When
227                        omitted all build configurations are used.
228                        ''')
229    parser.add_argument('--clean', action='store_true', default=False,
230                        help='clean existing build artifacts before operation.')
231    parser.add_argument('--report', action='store_true',
232                        help='generate code coverage report for tests.')
233    parser.add_argument('actions', nargs='*', choices=['build', 'test'],
234                        help='build: compile build tests, test: compile/run executable tests.')
235    parser.add_argument('--test-suite', default=None,
236                        help='select test suite to run')
237    parser.add_argument('--update-image', action='store_true', default=False,
238                        help='Update test image using LVGLImage.py script')
239
240    args = parser.parse_args()
241
242    if args.update_image:
243        generate_test_images()
244
245    if args.build_options:
246        options_to_build = args.build_options
247    else:
248        if 'build' in args.actions:
249            if 'test' in args.actions:
250                options_to_build = {**build_only_options, **test_options}
251            else:
252                options_to_build = build_only_options
253        else:
254            options_to_build = test_options
255
256    for options_name in options_to_build:
257        is_test = options_name in test_options
258        build_type = 'Debug'
259        build_tests(options_name, build_type, args.clean)
260        if is_test:
261            try:
262                run_tests(options_name, args.test_suite)
263            except subprocess.CalledProcessError as e:
264                sys.exit(e.returncode)
265
266    if args.report:
267        generate_code_coverage_report()
268