_clar.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. #!/usr/bin/env python
  2. # Copyright 2024 Google LLC
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. from __future__ import with_statement
  16. from string import Template
  17. import re, fnmatch, os
  18. VERSION = "0.10.0"
  19. TEST_FUNC_REGEX = r"^(void\s+(%s__(\w+))\(\s*void\s*\))\s*\{"
  20. EVENT_CB_REGEX = re.compile(
  21. r"^(void\s+clar_on_(\w+)\(\s*void\s*\))\s*\{",
  22. re.MULTILINE)
  23. SKIP_COMMENTS_REGEX = re.compile(
  24. r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"',
  25. re.DOTALL | re.MULTILINE)
  26. CATEGORY_REGEX = re.compile(r"CL_IN_CATEGORY\(\s*\"([^\"]+)\"\s*\)")
  27. CLAR_HEADER = """
  28. /*
  29. * Clar v%s
  30. *
  31. * This is an autogenerated file. Do not modify.
  32. * To add new unit tests or suites, regenerate the whole
  33. * file with `./clar`
  34. */
  35. """ % VERSION
  36. CLAR_EVENTS = [
  37. 'init',
  38. 'shutdown',
  39. 'test',
  40. 'suite'
  41. ]
  42. def main():
  43. from optparse import OptionParser
  44. parser = OptionParser()
  45. parser.add_option('-c', '--clar-path', dest='clar_path')
  46. parser.add_option('-v', '--report-to', dest='print_mode', default='default')
  47. parser.add_option('-f', '--file', dest='file')
  48. options, args = parser.parse_args()
  49. folder = args[0] or '.'
  50. print('folder: %s' % folder)
  51. builder = ClarTestBuilder(folder,
  52. clar_path = options.clar_path,
  53. print_mode = options.print_mode)
  54. if options.file is not None:
  55. builder.load_file(options.file)
  56. else:
  57. builder.load_dir(folder)
  58. builder.render()
  59. class ClarTestBuilder:
  60. def __init__(self, path, clar_path = None, print_mode = 'default'):
  61. self.declarations = []
  62. self.suite_names = []
  63. self.callback_data = {}
  64. self.suite_data = {}
  65. self.category_data = {}
  66. self.event_callbacks = []
  67. self.clar_path = os.path.abspath(clar_path) if clar_path else None
  68. self.path = os.path.abspath(path)
  69. self.modules = [
  70. "clar_sandbox.c",
  71. "clar_fixtures.c",
  72. "clar_fs.c",
  73. "clar_mock.c",
  74. "clar_categorize.c",
  75. ]
  76. self.modules.append("clar_print_%s.c" % print_mode)
  77. def load_dir(self, folder):
  78. print("Loading test suites...")
  79. for root, dirs, files in os.walk(self.path):
  80. module_root = root[len(self.path):]
  81. module_root = [c for c in module_root.split(os.sep) if c]
  82. tests_in_module = fnmatch.filter(files, "*.c")
  83. for test_file in tests_in_module:
  84. full_path = os.path.join(root, test_file)
  85. test_name = "_".join(module_root + [test_file[:-2]])
  86. with open(full_path) as f:
  87. self._process_test_file(test_name, f.read())
  88. def load_file(self, filename):
  89. with open(filename) as f:
  90. test_name = os.path.basename(filename)[:-2]
  91. self._process_test_file(test_name, f.read())
  92. def render(self):
  93. if not self.suite_data:
  94. raise RuntimeError('No tests found under "%s"' % self.path)
  95. if not os.path.isdir(self.path):
  96. os.makedirs(self.path)
  97. main_file = os.path.join(self.path, 'clar_main.c')
  98. with open(main_file, "w") as out:
  99. out.write(self._render_main())
  100. header_file = os.path.join(self.path, 'clar.h')
  101. with open(header_file, "w") as out:
  102. out.write(self._render_header())
  103. print ('Written Clar suite to "%s"' % self.path)
  104. #####################################################
  105. # Internal methods
  106. #####################################################
  107. def _render_cb(self, cb):
  108. return '{"%s", &%s}' % (cb['short_name'], cb['symbol'])
  109. def _render_suite(self, suite, index):
  110. template = Template(
  111. r"""
  112. {
  113. ${suite_index},
  114. "${clean_name}",
  115. ${initialize},
  116. ${cleanup},
  117. ${categories},
  118. ${cb_ptr}, ${cb_count}
  119. }
  120. """)
  121. callbacks = {}
  122. for cb in ['initialize', 'cleanup']:
  123. callbacks[cb] = (self._render_cb(suite[cb])
  124. if suite[cb] else "{NULL, NULL}")
  125. if len(self.category_data[suite['name']]) > 0:
  126. cats = "_clar_cat_%s" % suite['name']
  127. else:
  128. cats = "NULL"
  129. return template.substitute(
  130. suite_index = index,
  131. clean_name = suite['name'].replace("_", "::"),
  132. initialize = callbacks['initialize'],
  133. cleanup = callbacks['cleanup'],
  134. categories = cats,
  135. cb_ptr = "_clar_cb_%s" % suite['name'],
  136. cb_count = suite['cb_count']
  137. ).strip()
  138. def _render_callbacks(self, suite_name, callbacks):
  139. template = Template(
  140. r"""
  141. static const struct clar_func _clar_cb_${suite_name}[] = {
  142. ${callbacks}
  143. };
  144. """)
  145. callbacks = [
  146. self._render_cb(cb)
  147. for cb in callbacks
  148. if cb['short_name'] not in ('initialize', 'cleanup')
  149. ]
  150. return template.substitute(
  151. suite_name = suite_name,
  152. callbacks = ",\n\t".join(callbacks)
  153. ).strip()
  154. def _render_categories(self, suite_name, categories):
  155. template = Template(
  156. r"""
  157. static const char *_clar_cat_${suite_name}[] = { "${categories}", NULL };
  158. """)
  159. if len(categories) > 0:
  160. return template.substitute(
  161. suite_name = suite_name,
  162. categories = '","'.join(categories)
  163. ).strip()
  164. else:
  165. return ""
  166. def _render_event_overrides(self):
  167. overrides = []
  168. for event in CLAR_EVENTS:
  169. if event in self.event_callbacks:
  170. continue
  171. overrides.append(
  172. "#define clar_on_%s() /* nop */" % event
  173. )
  174. return '\n'.join(overrides)
  175. def _render_header(self):
  176. template = Template(self._load_file('clar.h'))
  177. declarations = "\n".join(
  178. "extern %s;" % decl
  179. for decl in sorted(self.declarations)
  180. )
  181. return template.substitute(
  182. extern_declarations = declarations,
  183. )
  184. def _render_main(self):
  185. template = Template(self._load_file('clar.c'))
  186. suite_names = sorted(self.suite_names)
  187. suite_data = [
  188. self._render_suite(self.suite_data[s], i)
  189. for i, s in enumerate(suite_names)
  190. ]
  191. callbacks = [
  192. self._render_callbacks(s, self.callback_data[s])
  193. for s in suite_names
  194. ]
  195. callback_count = sum(
  196. len(cbs) for cbs in self.callback_data.values()
  197. )
  198. categories = [
  199. self._render_categories(s, self.category_data[s])
  200. for s in suite_names
  201. ]
  202. return template.substitute(
  203. clar_modules = self._get_modules(),
  204. clar_callbacks = "\n".join(callbacks),
  205. clar_categories = "".join(categories),
  206. clar_suites = ",\n\t".join(suite_data),
  207. clar_suite_count = len(suite_data),
  208. clar_callback_count = callback_count,
  209. clar_event_overrides = self._render_event_overrides(),
  210. )
  211. def _load_file(self, filename):
  212. if self.clar_path:
  213. filename = os.path.join(self.clar_path, filename)
  214. with open(filename) as cfile:
  215. return cfile.read()
  216. else:
  217. import zlib, base64, sys
  218. content = CLAR_FILES[filename]
  219. if sys.version_info >= (3, 0):
  220. content = bytearray(content, 'utf_8')
  221. content = base64.b64decode(content)
  222. content = zlib.decompress(content)
  223. return str(content, 'utf-8')
  224. else:
  225. content = base64.b64decode(content)
  226. return zlib.decompress(content)
  227. def _get_modules(self):
  228. return "\n".join(self._load_file(f) for f in self.modules)
  229. def _skip_comments(self, text):
  230. def _replacer(match):
  231. s = match.group(0)
  232. return "" if s.startswith('/') else s
  233. return re.sub(SKIP_COMMENTS_REGEX, _replacer, text)
  234. def _process_test_file(self, suite_name, contents):
  235. contents = self._skip_comments(contents)
  236. self._process_events(contents)
  237. self._process_declarations(suite_name, contents)
  238. self._process_categories(suite_name, contents)
  239. def _process_events(self, contents):
  240. for (decl, event) in EVENT_CB_REGEX.findall(contents):
  241. if event not in CLAR_EVENTS:
  242. continue
  243. self.declarations.append(decl)
  244. self.event_callbacks.append(event)
  245. def _process_declarations(self, suite_name, contents):
  246. callbacks = []
  247. initialize = cleanup = None
  248. regex_string = TEST_FUNC_REGEX % suite_name
  249. regex = re.compile(regex_string, re.MULTILINE)
  250. for (declaration, symbol, short_name) in regex.findall(contents):
  251. data = {
  252. "short_name" : short_name,
  253. "declaration" : declaration,
  254. "symbol" : symbol
  255. }
  256. if short_name == 'initialize':
  257. initialize = data
  258. elif short_name == 'cleanup':
  259. cleanup = data
  260. else:
  261. callbacks.append(data)
  262. if not callbacks:
  263. return
  264. tests_in_suite = len(callbacks)
  265. suite = {
  266. "name" : suite_name,
  267. "initialize" : initialize,
  268. "cleanup" : cleanup,
  269. "cb_count" : tests_in_suite
  270. }
  271. if initialize:
  272. self.declarations.append(initialize['declaration'])
  273. if cleanup:
  274. self.declarations.append(cleanup['declaration'])
  275. self.declarations += [
  276. callback['declaration']
  277. for callback in callbacks
  278. ]
  279. callbacks.sort(key=lambda x: x['short_name'])
  280. self.callback_data[suite_name] = callbacks
  281. self.suite_data[suite_name] = suite
  282. self.suite_names.append(suite_name)
  283. print(" %s (%d tests)" % (suite_name, tests_in_suite))
  284. def _process_categories(self, suite_name, contents):
  285. self.category_data[suite_name] = [
  286. cat for cat in CATEGORY_REGEX.findall(contents) ]