pebble_test.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. # Copyright 2024 Google LLC
  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. from waflib.TaskGen import before, after, feature, taskgen_method
  15. from waflib import Errors, Logs, Options, Task, Utils, Node
  16. from waftools import junit_xml
  17. from string import Template
  18. import hashlib
  19. import json
  20. import lcov_info_parser
  21. import os
  22. import re
  23. import unicodedata as ud
  24. @feature('pebble_test')
  25. @after('apply_link')
  26. def make_test(self):
  27. if not 'cprogram' in self.features and not 'cxxprogram' in self.features:
  28. Logs.error('test cannot be executed %s'%self)
  29. return
  30. if getattr(self, 'link_task', None):
  31. sources = [self.link_task.outputs[0]]
  32. task = self.create_task('run_test', sources)
  33. runtime_deps = getattr(self.link_task.generator, 'runtime_deps', None)
  34. if runtime_deps is not None:
  35. task.dep_nodes = list(runtime_deps)
  36. # Lock to prevent concurrent modifications of the utest_results list. We may
  37. # have multiple tests running and finishing at the same time.
  38. import threading
  39. testlock = threading.Lock()
  40. class run_test(Task.Task):
  41. color = 'PINK'
  42. def runnable_status(self):
  43. if self.generator.bld.options.no_run:
  44. return Task.SKIP_ME
  45. ret = super(run_test, self).runnable_status()
  46. if ret==Task.SKIP_ME:
  47. # FIXME: We probably don't need to rerun tests if the inputs don't change, but meh, whatever.
  48. return Task.RUN_ME
  49. return ret
  50. def run_test(self, test_runme_node, cwd):
  51. # Execute the test normally:
  52. try:
  53. timer = Utils.Timer()
  54. filename = test_runme_node.abspath()
  55. args = [filename]
  56. if filename.endswith('.js'):
  57. args.insert(0, 'node')
  58. if self.generator.bld.options.test_name:
  59. args.append("-t%s" % (self.generator.bld.options.test_name))
  60. if self.generator.bld.options.list_tests:
  61. self.generator.bld.options.show_output = True
  62. args.append("-l")
  63. proc = Utils.subprocess.Popen(args, cwd=cwd, stderr=Utils.subprocess.PIPE,
  64. stdout=Utils.subprocess.PIPE)
  65. (stdout, stderr) = proc.communicate()
  66. except OSError:
  67. Logs.pprint('RED', 'Failed to run test: %s' % filename)
  68. return
  69. if self.generator.bld.options.show_output:
  70. print(stdout)
  71. print(stderr)
  72. tup = (test_runme_node, proc.returncode, stdout, stderr, str(timer))
  73. self.generator.utest_result = tup
  74. testlock.acquire()
  75. try:
  76. bld = self.generator.bld
  77. Logs.debug("ut: %r", tup)
  78. try:
  79. bld.utest_results.append(tup)
  80. except AttributeError:
  81. bld.utest_results = [tup]
  82. a = getattr(self.generator.bld, 'added_post_fun', False)
  83. if not a:
  84. self.generator.bld.add_post_fun(summary)
  85. self.generator.bld.added_post_fun = True
  86. finally:
  87. testlock.release()
  88. def run(self):
  89. test_runme_node = self.inputs[0]
  90. cwd = self.inputs[0].parent.abspath()
  91. if self.generator.bld.options.debug_test:
  92. # Only debug the first test encountered. In case the -M option was
  93. # omitted or a lot of tests were matched, it would otherwise result
  94. # in repeatedly launching the debugger... poor dev xp :)
  95. is_added = getattr(self.generator.bld, 'added_debug_fun', False)
  96. if not is_added:
  97. # Create a post-build closure to execute:
  98. test_filename_abspath = test_runme_node.abspath()
  99. if test_filename_abspath.endswith('.js'):
  100. fmt = 'node-debug {ARGS}'
  101. cmd = fmt.format(ARGS=test_filename_abspath)
  102. else:
  103. build_dir = self.generator.bld.bldnode.abspath()
  104. fmt = 'gdb --cd={CWD} --directory={BLD_DIR} --args {ARGS}'
  105. cmd = fmt.format(CWD=cwd, BLD_DIR=build_dir,
  106. ARGS=test_filename_abspath)
  107. def debug_test(bld):
  108. # Execute the test within gdb for debugging:
  109. os.system(cmd)
  110. self.generator.bld.add_post_fun(debug_test)
  111. self.generator.bld.added_debug_fun = True
  112. else:
  113. Logs.pprint('RED', 'More than one test was selected! '
  114. 'Debugging only the first one encountered...')
  115. else:
  116. self.run_test(test_runme_node, cwd)
  117. def summary(bld):
  118. lst = getattr(bld, 'utest_results', [])
  119. if not lst: return
  120. # Write a jUnit xml report for further processing by Jenkins:
  121. test_suites = []
  122. for (node, code, stdout, stderr, duration) in lst:
  123. # FIXME: We don't get a status per test, only at the suite level...
  124. # Perhaps clar itself should do the reporting?
  125. def strip_non_ascii(s):
  126. return "".join(i for i in str(s) if ord(i) < 128)
  127. test_case = junit_xml.TestCase('all')
  128. if code:
  129. # Include stdout and stderr if test failed:
  130. test_case.stdout = strip_non_ascii(stdout.decode("utf-8"))
  131. test_case.stderr = strip_non_ascii(stderr.decode("utf-8"))
  132. test_case.add_failure_info(message='failed')
  133. suite_name = node.parent.relpath()
  134. test_suite = junit_xml.TestSuite(suite_name, [test_case])
  135. test_suites.append(test_suite)
  136. report_xml_string = junit_xml.TestSuite.to_xml_string(test_suites)
  137. bld.bldnode.make_node('junit.xml').write(report_xml_string)
  138. total = len(lst)
  139. fail = len([x for x in lst if x[1]])
  140. Logs.pprint('CYAN', 'test summary')
  141. Logs.pprint('CYAN', ' tests that pass %d/%d' % (total-fail, total))
  142. for (node, code, out, err, duration) in lst:
  143. if not code:
  144. Logs.pprint('GREEN', ' %s' % node.abspath())
  145. if fail > 0:
  146. Logs.pprint('RED', ' tests that fail %d/%d' % (fail, total))
  147. for (node, code, out, err, duration) in lst:
  148. if code:
  149. Logs.pprint('RED', ' %s' % node.abspath())
  150. # FIXME: Make UTF-8 print properly, see PBL-29528
  151. print(ud.normalize('NFKD', out.decode('utf-8')))
  152. print(ud.normalize('NFKD', err.decode('utf-8')))
  153. raise Errors.WafError('test failed')
  154. @taskgen_method
  155. @feature("test_product_source")
  156. def test_product_source_hook(self):
  157. """ This function is a "task generator". It's going to generate one or more tasks to actually
  158. build our objects.
  159. """
  160. # Create a "c" task with the given inputs and outputs. This will use the class named "c"
  161. # defined in the waflib/Tools/c.py file provided by waf.
  162. self.create_task('c', self.product_src, self.product_out)
  163. def build_product_source_files(bld, test_dir, include_paths, defines, cflags, product_sources):
  164. """ Build the "product sources", which are the parts of our code base that are under test
  165. as well as any fakes we need to link against as well.
  166. Return a list of the compiled object nodes that we should later link against.
  167. This function attempts to share object files with other tests that use the same product
  168. sources and with the same compilation configuration. We can't always reuse objects
  169. because two tests might use different defines or include paths, but where we can we do.
  170. """
  171. top_dir = bld.root.find_dir(bld.top_dir)
  172. # Hash the configuration information. Some lists are order dependent, some aren't. When they're not
  173. # order dependent sort them so we have a higher likelihood of colliding and finding an existing
  174. # object file for this.
  175. h = hashlib.md5()
  176. h.update(Utils.h_list(include_paths))
  177. h.update(Utils.h_list(sorted(defines)))
  178. h.update(Utils.h_list(sorted(cflags)))
  179. compile_args_hash_str = h.hexdigest()
  180. if not hasattr(bld, 'utest_product_sources'):
  181. bld.utest_product_sources = set()
  182. product_objects = []
  183. for s in product_sources:
  184. # Make sure everything in the list is a node
  185. if isinstance(s, str):
  186. src_node = bld.path.find_node(s)
  187. else:
  188. src_node = s
  189. rel_path = src_node.path_from(top_dir)
  190. bld_args_dir = top_dir.get_bld().find_or_declare(compile_args_hash_str)
  191. out_node = bld_args_dir.find_or_declare(rel_path).change_ext('.o')
  192. product_objects.append(out_node)
  193. if out_node not in bld.utest_product_sources:
  194. # If we got here that means that we haven't built this product source yet. Build it now.
  195. bld.utest_product_sources.add(out_node)
  196. bld(features="test_product_source c",
  197. product_src=src_node,
  198. product_out=out_node,
  199. includes=include_paths,
  200. cflags=cflags,
  201. defines=defines)
  202. return product_objects
  203. def get_bitdepth_for_platform(bld, platform):
  204. if platform in ('snowy', 'spalding', 'robert'):
  205. return 8
  206. elif platform in ('tintin', 'silk'):
  207. return 1
  208. else:
  209. bld.fatal('Unknown platform {}'.format(platform))
  210. def add_clar_test(bld, test_name, test_source, sources_ant_glob, product_sources, test_libs,
  211. override_includes, add_includes, defines, runtime_deps, platform, use):
  212. if not bld.options.regex and bld.variant == 'test_rocky_emx':
  213. # Include tests starting with test_rocky... only!
  214. bld.options.regex = 'test_rocky'
  215. if (bld.options.regex):
  216. filename = str(test_source).strip()
  217. if not re.match(bld.options.regex, filename):
  218. return
  219. platform_set = set(['default', 'tintin', 'snowy', 'spalding', 'silk', 'robert'])
  220. #validate platforms specified
  221. if platform not in platform_set:
  222. raise ValueError("Invalid platform {} specified, valid platforms are {}".format(
  223. platform, ', '.join(platform_set)))
  224. platform_product_sources = list(product_sources)
  225. platform = platform.lower()
  226. platform_defines = []
  227. if platform == 'default':
  228. test_dir = bld.path.get_bld().make_node(test_name)
  229. node_name = 'runme'
  230. if bld.variant == 'test_rocky_emx':
  231. node_name += '.js'
  232. test_bin = test_dir.make_node(node_name)
  233. platform = 'snowy'
  234. # add a default platform define so file selection can use non-platform pbi/png files
  235. platform_defines.append('PLATFORM_DEFAULT=1')
  236. else:
  237. test_dir = bld.path.get_bld().make_node(test_name + '_' + platform)
  238. test_bin = test_dir.make_node('runme_' + platform)
  239. platform_defines.append('PLATFORM_DEFAULT=0')
  240. if platform == 'silk' or platform == 'robert':
  241. platform_defines.append('CAPABILITY_HAS_PUTBYTES_PREACKING=1')
  242. def _generate_clar_harness(task):
  243. bld = task.generator.bld
  244. clar_dir = task.generator.env.CLAR_DIR
  245. test_src_file = task.inputs[0].abspath()
  246. test_bld_dir = task.outputs[0].get_bld().parent.abspath()
  247. cmd = 'python {0}/clar.py --file={1} --clar-path={0} {2}'.format(clar_dir, test_src_file, test_bld_dir)
  248. task.generator.bld.exec_command(cmd)
  249. clar_harness = test_dir.make_node('clar_main.c')
  250. # Should make this a general task like the objcopy ones.
  251. bld(name='generate_clar_harness',
  252. rule=_generate_clar_harness,
  253. source=test_source,
  254. target=[clar_harness, test_dir.make_node('clar.h')])
  255. src_includes = [ "tests/overrides/default",
  256. "tests/stubs",
  257. "tests/fakes",
  258. "tests/test_includes",
  259. "tests",
  260. "src/include",
  261. "src/core",
  262. "src/fw",
  263. "src/libbtutil/include",
  264. "src/libos/include",
  265. "src/libutil/includes",
  266. "src/boot",
  267. "src/fw/applib/vendor/tinflate",
  268. "src/fw/applib/vendor/uPNG",
  269. "src/fw/vendor/jerryscript/jerry-core",
  270. "src/fw/vendor/jerryscript/jerry-core/jcontext",
  271. "src/fw/vendor/jerryscript/jerry-core/jmem",
  272. "src/fw/vendor/jerryscript/jerry-core/jrt",
  273. "src/fw/vendor/jerryscript/jerry-core/lit",
  274. "src/fw/vendor/jerryscript/jerry-core/vm",
  275. "src/fw/vendor/jerryscript/jerry-core/ecma/builtin-objects",
  276. "src/fw/vendor/jerryscript/jerry-core/ecma/base",
  277. "src/fw/vendor/jerryscript/jerry-core/ecma/operations",
  278. "src/fw/vendor/jerryscript/jerry-core/parser/js",
  279. "src/fw/vendor/jerryscript/jerry-core/parser/regexp",
  280. "third_party/freertos",
  281. "third_party/freertos/FreeRTOS-Kernel/FreeRTOS/Source/include",
  282. "third_party/freertos/FreeRTOS-Kernel/FreeRTOS/Source/portable/GCC/ARM_CM3",
  283. "src/fw/vendor/nanopb" ]
  284. # Use Snowy's resource headers as a fallback if we don't override it here
  285. resource_override_dir_name = platform if platform in ('silk', 'robert') else 'snowy'
  286. src_includes.append("tests/overrides/default/resources/{}".format(resource_override_dir_name))
  287. override_includes = ['tests/overrides/' + f for f in override_includes]
  288. src_includes = override_includes + src_includes
  289. if add_includes is not None:
  290. src_includes.extend(add_includes)
  291. src_includes = [os.path.join(bld.srcnode.abspath(), f) for f in src_includes]
  292. includes = src_includes
  293. # Add the generated IDL headers
  294. root_build_dir = bld.path.get_bld().abspath().replace(bld.path.relpath(), '')
  295. idl_includes = [root_build_dir + 'src/idl']
  296. includes += idl_includes
  297. if use is None:
  298. use = []
  299. # Add DUMA for memory corruption checking
  300. # conditionally disable duma based on DUMA_DISABLED being defined
  301. # DUMA is found in tests/vendor/duma
  302. use += ['libutil', 'libutil_includes', 'libos_includes', 'libbtutil', 'libbtutil_includes']
  303. if 'DUMA_DISABLED' not in defines and 'DUMA_DISABLED' not in bld.env.DEFINES:
  304. use.append('duma')
  305. test_libs.append('pthread') # DUMA depends on pthreads
  306. test_libs.append('m') # Add libm math.h functions
  307. # pulling in display.h and display_<platform>.h
  308. # we force include these per platform so platform specific code using
  309. # ifdefs are triggered correctly without reconfiguring/rebuilding all unit tests per platform
  310. board_path = bld.srcnode.find_node('src/fw/board').abspath()
  311. util_path = bld.srcnode.find_node('src/fw/util').abspath()
  312. bitdepth = get_bitdepth_for_platform(bld, platform)
  313. cflags_force_include = ['-Wno-unused-command-line-argument']
  314. cflags_force_include.append('-include' + board_path + '/displays/display_' + platform + '.h')
  315. platform_defines += ['PLATFORM_' + platform.upper(), 'PLATFORM_NAME="%s"' % platform] +\
  316. ['SCREEN_COLOR_DEPTH_BITS=%d' % bitdepth]
  317. if sources_ant_glob is not None:
  318. platform_sources_ant_glob = sources_ant_glob
  319. # handle platform specific files (ex. display_${PLATFORM}.c)
  320. platform_sources_ant_glob = Template(platform_sources_ant_glob).substitute(
  321. PLATFORM=platform, BITDEPTH=bitdepth)
  322. sources_list = Utils.to_list(platform_sources_ant_glob)
  323. for s in sources_list:
  324. node = bld.srcnode.find_node(s)
  325. if node is None:
  326. raise Errors.WafError('Error: Source file "%s" not found for "%s"' % (s, test_name))
  327. if node not in platform_product_sources:
  328. platform_product_sources.append(node)
  329. else:
  330. raise Errors.WafError('Error: Duplicate source file "%s" found for "%s"' % (s, test_name))
  331. program_sources = [test_source, clar_harness]
  332. program_sources.extend(build_product_source_files(
  333. bld, test_dir, includes, defines + platform_defines, cflags_force_include,
  334. platform_product_sources))
  335. bld.program(source=program_sources,
  336. target=test_bin,
  337. features='pebble_test',
  338. includes=[test_dir.abspath()] + includes,
  339. lib=test_libs,
  340. defines=defines + platform_defines,
  341. cflags=cflags_force_include,
  342. use=use,
  343. runtime_deps=runtime_deps)
  344. def clar(bld, sources=None, sources_ant_glob=None, test_sources_ant_glob=None,
  345. test_sources=None, test_libs=[], override_includes=[], add_includes=None, defines=None,
  346. test_name=None, runtime_deps=None, platforms=None, use=None):
  347. if test_sources_ant_glob is None and not test_sources:
  348. raise Exception()
  349. if test_sources_ant_glob in bld.env.BROKEN_TESTS:
  350. Logs.pprint('RED', f'Skipping glob because it is in the BROKEN_TESTS list: {test_sources_ant_glob}')
  351. return
  352. if test_sources is None:
  353. test_sources = []
  354. # Make a copy so if we modify it we don't accidentally modify the callers list
  355. defines = list(defines or [])
  356. defines.append('UNITTEST')
  357. if platforms is None:
  358. platforms = ['default']
  359. if sources is None:
  360. sources = []
  361. if test_sources_ant_glob:
  362. glob_sources = bld.path.ant_glob(test_sources_ant_glob)
  363. test_sources.extend([s for s in glob_sources if not os.path.basename(s.abspath()).startswith('clar')])
  364. Logs.debug("ut: Test sources %r", test_sources)
  365. if len(test_sources) == 0:
  366. Logs.pprint('RED', 'No tests found for glob: %s' % test_sources_ant_glob)
  367. for test_source in test_sources:
  368. if test_name is None:
  369. test_name = test_source.name
  370. test_name = test_name[:test_name.rfind('.')] # Scrape the extension
  371. for platform in platforms:
  372. add_clar_test(bld, test_name, test_source, sources_ant_glob, sources, test_libs,
  373. override_includes, add_includes, defines, runtime_deps, platform, use)