main.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. #!/usr/bin/env python3
  2. import argparse
  3. import errno
  4. import glob
  5. import shutil
  6. import subprocess
  7. import sys
  8. import os
  9. lvgl_test_dir = os.path.dirname(os.path.realpath(__file__))
  10. # Key values must match variable names in CMakeLists.txt.
  11. build_only_options = {
  12. 'OPTIONS_MINIMAL_MONOCHROME': 'Minimal config monochrome',
  13. 'OPTIONS_NORMAL_8BIT': 'Normal config, 8 bit color depth',
  14. 'OPTIONS_16BIT': 'Minimal config, 16 bit color depth',
  15. 'OPTIONS_16BIT_SWAP': 'Normal config, 16 bit color depth swapped',
  16. 'OPTIONS_FULL_32BIT': 'Full config, 32 bit color depth',
  17. }
  18. test_options = {
  19. 'OPTIONS_TEST_SYSHEAP': 'Test config, system heap, 32 bit color depth',
  20. 'OPTIONS_TEST_DEFHEAP': 'Test config, LVGL heap, 32 bit color depth',
  21. }
  22. def is_valid_option_name(option_name):
  23. return option_name in build_only_options or option_name in test_options
  24. def get_option_description(option_name):
  25. if option_name in build_only_options:
  26. return build_only_options[option_name]
  27. return test_options[option_name]
  28. def delete_dir_ignore_missing(dir_path):
  29. '''Recursively delete a directory and ignore if missing.'''
  30. try:
  31. shutil.rmtree(dir_path)
  32. except FileNotFoundError:
  33. pass
  34. def generate_test_runners():
  35. '''Generate the test runner source code.'''
  36. global lvgl_test_dir
  37. os.chdir(lvgl_test_dir)
  38. delete_dir_ignore_missing('src/test_runners')
  39. os.mkdir('src/test_runners')
  40. # TODO: Intermediate files should be in the build folders, not alongside
  41. # the other repo source.
  42. for f in glob.glob("./src/test_cases/test_*.c"):
  43. r = f[:-2] + "_Runner.c"
  44. r = r.replace("/test_cases/", "/test_runners/")
  45. subprocess.check_call(['ruby', 'unity/generate_test_runner.rb',
  46. f, r, 'config.yml'])
  47. def options_abbrev(options_name):
  48. '''Return an abbreviated version of the option name.'''
  49. prefix = 'OPTIONS_'
  50. assert options_name.startswith(prefix)
  51. return options_name[len(prefix):].lower()
  52. def get_base_buid_dir(options_name):
  53. '''Given the build options name, return the build directory name.
  54. Does not return the full path to the directory - just the base name.'''
  55. return 'build_%s' % options_abbrev(options_name)
  56. def get_build_dir(options_name):
  57. '''Given the build options name, return the build directory name.
  58. Returns absolute path to the build directory.'''
  59. global lvgl_test_dir
  60. return os.path.join(lvgl_test_dir, get_base_buid_dir(options_name))
  61. def build_tests(options_name, build_type, clean):
  62. '''Build all tests for the specified options name.'''
  63. global lvgl_test_dir
  64. print()
  65. print()
  66. label = 'Building: %s: %s' % (options_abbrev(
  67. options_name), get_option_description(options_name))
  68. print('=' * len(label))
  69. print(label)
  70. print('=' * len(label))
  71. print(flush=True)
  72. build_dir = get_build_dir(options_name)
  73. if clean:
  74. delete_dir_ignore_missing(build_dir)
  75. os.chdir(lvgl_test_dir)
  76. created_build_dir = False
  77. if not os.path.isdir(build_dir):
  78. os.mkdir(build_dir)
  79. created_build_dir = True
  80. os.chdir(build_dir)
  81. if created_build_dir:
  82. subprocess.check_call(['cmake', '-DCMAKE_BUILD_TYPE=%s' % build_type,
  83. '-D%s=1' % options_name, '..'])
  84. subprocess.check_call(['cmake', '--build', build_dir,
  85. '--parallel', str(os.cpu_count())])
  86. def run_tests(options_name):
  87. '''Run the tests for the given options name.'''
  88. print()
  89. print()
  90. label = 'Running tests for %s' % options_abbrev(options_name)
  91. print('=' * len(label))
  92. print(label)
  93. print('=' * len(label), flush=True)
  94. os.chdir(get_build_dir(options_name))
  95. subprocess.check_call(
  96. ['ctest', '--timeout', '30', '--parallel', str(os.cpu_count()), '--output-on-failure'])
  97. def generate_code_coverage_report():
  98. '''Produce code coverage test reports for the test execution.'''
  99. global lvgl_test_dir
  100. print()
  101. print()
  102. label = 'Generating code coverage reports'
  103. print('=' * len(label))
  104. print(label)
  105. print('=' * len(label))
  106. print(flush=True)
  107. os.chdir(lvgl_test_dir)
  108. delete_dir_ignore_missing('report')
  109. os.mkdir('report')
  110. root_dir = os.pardir
  111. html_report_file = 'report/index.html'
  112. cmd = ['gcovr', '--root', root_dir, '--html-details', '--output',
  113. html_report_file, '--xml', 'report/coverage.xml',
  114. '-j', str(os.cpu_count()), '--print-summary',
  115. '--html-title', 'LVGL Test Coverage']
  116. for d in ('.*\\bexamples/.*', '\\bsrc/test_.*', '\\bsrc/lv_test.*', '\\bunity\\b'):
  117. cmd.extend(['--exclude', d])
  118. subprocess.check_call(cmd)
  119. print("Done: See %s" % html_report_file, flush=True)
  120. if __name__ == "__main__":
  121. epilog = '''This program builds and optionally runs the LVGL test programs.
  122. There are two types of LVGL tests: "build", and "test". The build-only
  123. tests, as their name suggests, only verify that the program successfully
  124. compiles and links (with various build options). There are also a set of
  125. tests that execute to verify correct LVGL library behavior.
  126. '''
  127. parser = argparse.ArgumentParser(
  128. description='Build and/or run LVGL tests.', epilog=epilog)
  129. parser.add_argument('--build-options', nargs=1,
  130. help='''the build option name to build or run. When
  131. omitted all build configurations are used.
  132. ''')
  133. parser.add_argument('--clean', action='store_true', default=False,
  134. help='clean existing build artifacts before operation.')
  135. parser.add_argument('--report', action='store_true',
  136. help='generate code coverage report for tests.')
  137. parser.add_argument('actions', nargs='*', choices=['build', 'test'],
  138. help='build: compile build tests, test: compile/run executable tests.')
  139. args = parser.parse_args()
  140. if args.build_options:
  141. options_to_build = args.build_options
  142. else:
  143. if 'build' in args.actions:
  144. if 'test' in args.actions:
  145. options_to_build = {**build_only_options, **test_options}
  146. else:
  147. options_to_build = build_only_options
  148. else:
  149. options_to_build = test_options
  150. for opt in options_to_build:
  151. if not is_valid_option_name(opt):
  152. print('Invalid build option "%s"' % opt, file=sys.stderr)
  153. sys.exit(errno.EINVAL)
  154. generate_test_runners()
  155. for options_name in options_to_build:
  156. is_test = options_name in test_options
  157. build_type = 'Debug'
  158. build_tests(options_name, build_type, args.clean)
  159. if is_test:
  160. try:
  161. run_tests(options_name)
  162. except subprocess.CalledProcessError as e:
  163. sys.exit(e.returncode)
  164. if args.report:
  165. generate_code_coverage_report()