3 # Copyright (c) Vicent Marti. All rights reserved.
5 # This file is part of clar, distributed under the ISC license.
6 # For full terms see the included COPYING file.
9 from __future__
import with_statement
10 from string
import Template
11 import re
, fnmatch
, os
, sys
, codecs
, pickle
14 class Template(object):
15 def __init__(self
, module
):
18 def _render_callback(self
, cb
):
20 return ' { NULL, NULL }'
21 return ' { "%s", &%s }' % (cb
['short_name'], cb
['symbol'])
23 class DeclarationTemplate(Template
):
25 out
= "\n".join("extern %s;" % cb
['declaration'] for cb
in self
.module
.callbacks
) + "\n"
27 for initializer
in self
.module
.initializers
:
28 out
+= "extern %s;\n" % initializer
['declaration']
30 if self
.module
.cleanup
:
31 out
+= "extern %s;\n" % self
.module
.cleanup
['declaration']
35 class CallbacksTemplate(Template
):
37 out
= "static const struct clar_func _clar_cb_%s[] = {\n" % self
.module
.name
38 out
+= ",\n".join(self
._render
_callback
(cb
) for cb
in self
.module
.callbacks
)
42 class InfoTemplate(Template
):
46 initializers
= self
.module
.initializers
47 if len(initializers
) == 0:
48 initializers
= [ None ]
50 for initializer
in initializers
:
51 name
= self
.module
.clean_name()
52 if initializer
and initializer
['short_name'].startswith('initialize_'):
53 variant
= initializer
['short_name'][len('initialize_'):]
54 name
+= " (%s)" % variant
.replace('_', ' ')
62 ${cb_ptr}, ${cb_count}, ${enabled}
66 initialize
= self
._render
_callback
(initializer
),
67 cleanup
= self
._render
_callback
(self
.module
.cleanup
),
68 cb_ptr
= "_clar_cb_%s" % self
.module
.name
,
69 cb_count
= len(self
.module
.callbacks
),
70 enabled
= int(self
.module
.enabled
)
72 templates
.append(template
)
74 return ','.join(templates
)
76 def __init__(self
, name
):
84 return self
.name
.replace("_", "::")
86 def _skip_comments(self
, text
):
87 SKIP_COMMENTS_REGEX
= re
.compile(
88 r
'//.*?$|/\*.*?\*/|\'(?
:\\.|
[^
\\\'])*\'|
"(?:\\.|[^\\"])*"',
89 re.DOTALL | re.MULTILINE)
93 return "" if s.startswith('/') else s
95 return re.sub(SKIP_COMMENTS_REGEX, _replacer, text)
97 def parse(self, contents):
98 TEST_FUNC_REGEX = r"^
(void\s
+(test_
%s__(\w
+))\s
*\
(\s
*void\s
*\
))\s
*\
{"
100 contents = self._skip_comments(contents)
101 regex = re.compile(TEST_FUNC_REGEX % self.name, re.MULTILINE)
104 self.initializers = []
107 for (declaration, symbol, short_name) in regex.findall(contents):
109 "short_name
" : short_name,
110 "declaration
" : declaration,
114 if short_name.startswith('initialize'):
115 self.initializers.append(data)
116 elif short_name == 'cleanup':
119 self.callbacks.append(data)
121 return self.callbacks != []
123 def refresh(self, path):
124 self.modified = False
130 if st.st_mtime == self.mtime:
134 self.mtime = st.st_mtime
136 with codecs.open(path, encoding='utf-8') as fp:
137 raw_content = fp.read()
142 return self.parse(raw_content)
144 class TestSuite(object):
146 def __init__(self, path, output):
150 def should_generate(self, path):
151 if not os.path.isfile(path):
154 if any(module.modified for module in self.modules.values()):
159 def find_modules(self):
161 for root, _, files in os.walk(self.path):
162 module_root = root[len(self.path):]
163 module_root = [c for c in module_root.split(os.sep) if c]
165 tests_in_module = fnmatch.filter(files, "*.c
")
167 for test_file in tests_in_module:
168 full_path = os.path.join(root, test_file)
169 module_name = "_
".join(module_root + [test_file[:-2]]).replace("-", "_
")
171 modules.append((full_path, module_name))
175 def load_cache(self):
176 path = os.path.join(self.output, '.clarcache')
180 fp = open(path, 'rb')
181 cache = pickle.load(fp)
183 except (IOError, ValueError):
188 def save_cache(self):
189 path = os.path.join(self.output, '.clarcache')
190 with open(path, 'wb') as cache:
191 pickle.dump(self.modules, cache)
193 def load(self, force = False):
194 module_data = self.find_modules()
195 self.modules = {} if force else self.load_cache()
197 for path, name in module_data:
198 if name not in self.modules:
199 self.modules[name] = Module(name)
201 if not self.modules[name].refresh(path):
202 del self.modules[name]
204 def disable(self, excluded):
205 for exclude in excluded:
206 for module in self.modules.values():
207 name = module.clean_name()
208 if name.startswith(exclude):
209 module.enabled = False
210 module.modified = True
212 def suite_count(self):
213 return sum(max(1, len(m.initializers)) for m in self.modules.values())
215 def callback_count(self):
216 return sum(len(module.callbacks) for module in self.modules.values())
219 output = os.path.join(self.output, 'clar.suite')
221 if not self.should_generate(output):
224 with open(output, 'w') as data:
225 modules = sorted(self.modules.values(), key=lambda module: module.name)
227 for module in modules:
228 t = Module.DeclarationTemplate(module)
229 data.write(t.render())
231 for module in modules:
232 t = Module.CallbacksTemplate(module)
233 data.write(t.render())
235 suites = "static struct clar_suite _clar_suites
[] = {" + ','.join(
236 Module.InfoTemplate(module).render() for module in modules
241 data.write("static const size_t _clar_suite_count
= %d;\n" % self.suite_count())
242 data.write("static const size_t _clar_callback_count
= %d;\n" % self.callback_count())
247 if __name__ == '__main__':
248 from optparse import OptionParser
250 parser = OptionParser()
251 parser.add_option('-f', '--force', action="store_true
", dest='force', default=False)
252 parser.add_option('-x', '--exclude', dest='excluded', action='append', default=[])
253 parser.add_option('-o', '--output', dest='output')
255 options, args = parser.parse_args()
257 print("More than one path given
")
260 path = args.pop() if args else '.'
261 output = options.output or path
262 suite = TestSuite(path, output)
263 suite.load(options.force)
264 suite.disable(options.excluded)
266 print("Written `clar
.suite`
(%d tests
in %d suites
)" % (suite.callback_count(), suite.suite_count()))