Raw
1 #!/usr/bin/env python
2 #
3 # Copyright (c) Vicent Marti. All rights reserved.
4 #
5 # This file is part of clar, distributed under the ISC license.
6 # For full terms see the included COPYING file.
7 #
8
9 from __future__ import with_statement
10 from string import Template
11 import re, fnmatch, os, sys, codecs, pickle, io
12
13 class Module(object):
14 class Template(object):
15 def __init__(self, module):
16 self.module = module
17
18 def _render_callback(self, cb):
19 if not cb:
20 return ' { NULL, NULL }'
21 return ' { "%s", &%s }' % (cb['short_name'], cb['symbol'])
22
23 class DeclarationTemplate(Template):
24 def render(self):
25 out = "\n".join("extern %s;" % cb['declaration'] for cb in self.module.callbacks) + "\n"
26
27 for initializer in self.module.initializers:
28 out += "extern %s;\n" % initializer['declaration']
29
30 if self.module.cleanup:
31 out += "extern %s;\n" % self.module.cleanup['declaration']
32
33 return out
34
35 class CallbacksTemplate(Template):
36 def render(self):
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)
39 out += "\n};\n"
40 return out
41
42 class InfoTemplate(Template):
43 def render(self):
44 templates = []
45
46 initializers = self.module.initializers
47 if len(initializers) == 0:
48 initializers = [ None ]
49
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('_', ' ')
55
56 template = Template(
57 r"""
58 {
59 "${clean_name}",
60 ${initialize},
61 ${cleanup},
62 ${cb_ptr}, ${cb_count}, ${enabled}
63 }"""
64 ).substitute(
65 clean_name = name,
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)
71 )
72 templates.append(template)
73
74 return ','.join(templates)
75
76 def __init__(self, name):
77 self.name = name
78
79 self.mtime = None
80 self.enabled = True
81 self.modified = False
82
83 def clean_name(self):
84 return self.name.replace("_", "::")
85
86 def _skip_comments(self, text):
87 SKIP_COMMENTS_REGEX = re.compile(
88 r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"',
89 re.DOTALL | re.MULTILINE)
90
91 def _replacer(match):
92 s = match.group(0)
93 return "" if s.startswith('/') else s
94
95 return re.sub(SKIP_COMMENTS_REGEX, _replacer, text)
96
97 def parse(self, contents):
98 TEST_FUNC_REGEX = r"^(void\s+(test_%s__(\w+))\s*\(\s*void\s*\))\s*\{"
99
100 contents = self._skip_comments(contents)
101 regex = re.compile(TEST_FUNC_REGEX % self.name, re.MULTILINE)
102
103 self.callbacks = []
104 self.initializers = []
105 self.cleanup = None
106
107 for (declaration, symbol, short_name) in regex.findall(contents):
108 data = {
109 "short_name" : short_name,
110 "declaration" : declaration,
111 "symbol" : symbol
112 }
113
114 if short_name.startswith('initialize'):
115 self.initializers.append(data)
116 elif short_name == 'cleanup':
117 self.cleanup = data
118 else:
119 self.callbacks.append(data)
120
121 return self.callbacks != []
122
123 def refresh(self, path):
124 self.modified = False
125
126 try:
127 st = os.stat(path)
128
129 # Not modified
130 if st.st_mtime == self.mtime:
131 return True
132
133 self.modified = True
134 self.mtime = st.st_mtime
135
136 with codecs.open(path, encoding='utf-8') as fp:
137 raw_content = fp.read()
138
139 except IOError:
140 return False
141
142 return self.parse(raw_content)
143
144 class TestSuite(object):
145
146 def __init__(self, path, output):
147 self.path = path
148 self.output = output
149
150 def maybe_generate(self, path):
151 if not os.path.isfile(path):
152 return True
153
154 if any(module.modified for module in self.modules.values()):
155 return True
156
157 return False
158
159 def find_modules(self):
160 modules = []
161
162 if os.path.isfile(self.path):
163 full_path = os.path.abspath(self.path)
164 module_name = os.path.basename(self.path)
165 module_name = os.path.splitext(module_name)[0]
166 modules.append((full_path, module_name))
167 else:
168 for root, _, files in os.walk(self.path):
169 module_root = root[len(self.path):]
170 module_root = [c for c in module_root.split(os.sep) if c]
171
172 tests_in_module = fnmatch.filter(files, "*.c")
173
174 for test_file in tests_in_module:
175 full_path = os.path.join(root, test_file)
176 module_name = "_".join(module_root + [test_file[:-2]]).replace("-", "_")
177
178 modules.append((full_path, module_name))
179
180 return modules
181
182 def load_cache(self):
183 path = os.path.join(self.output, '.clarcache')
184 cache = {}
185
186 try:
187 fp = open(path, 'rb')
188 cache = pickle.load(fp)
189 fp.close()
190 except (IOError, ValueError):
191 pass
192
193 return cache
194
195 def save_cache(self):
196 path = os.path.join(self.output, '.clarcache')
197 with open(path, 'wb') as cache:
198 pickle.dump(self.modules, cache)
199
200 def load(self, force = False):
201 module_data = self.find_modules()
202 self.modules = {} if force else self.load_cache()
203
204 for path, name in module_data:
205 if name not in self.modules:
206 self.modules[name] = Module(name)
207
208 if not self.modules[name].refresh(path):
209 del self.modules[name]
210
211 def disable(self, excluded):
212 for exclude in excluded:
213 for module in self.modules.values():
214 name = module.clean_name()
215 if name.startswith(exclude):
216 module.enabled = False
217 module.modified = True
218
219 def suite_count(self):
220 return sum(max(1, len(m.initializers)) for m in self.modules.values())
221
222 def callback_count(self):
223 return sum(len(module.callbacks) for module in self.modules.values())
224
225 def write(self):
226 if not os.path.exists(self.output):
227 os.makedirs(self.output)
228
229 wrote_suite = self.write_suite()
230 wrote_header = self.write_header()
231
232 if wrote_suite or wrote_header:
233 self.save_cache()
234 return True
235
236 return False
237
238 def write_output(self, fn, data):
239 if not self.maybe_generate(fn):
240 return False
241
242 current = None
243
244 try:
245 with open(fn, 'r') as input:
246 current = input.read()
247 except OSError:
248 pass
249 except IOError:
250 pass
251
252 if current == data:
253 return False
254
255 with open(fn, 'w') as output:
256 output.write(data)
257
258 return True
259
260 def write_suite(self):
261 suite_fn = os.path.join(self.output, 'clar.suite')
262
263 with io.StringIO() as suite_file:
264 modules = sorted(self.modules.values(), key=lambda module: module.name)
265
266 for module in modules:
267 t = Module.DeclarationTemplate(module)
268 suite_file.write(t.render())
269
270 for module in modules:
271 t = Module.CallbacksTemplate(module)
272 suite_file.write(t.render())
273
274 suites = "static struct clar_suite _clar_suites[] = {" + ','.join(
275 Module.InfoTemplate(module).render() for module in modules
276 ) + "\n};\n"
277
278 suite_file.write(suites)
279
280 suite_file.write(u"static const size_t _clar_suite_count = %d;\n" % self.suite_count())
281 suite_file.write(u"static const size_t _clar_callback_count = %d;\n" % self.callback_count())
282
283 return self.write_output(suite_fn, suite_file.getvalue())
284
285 return False
286
287 def write_header(self):
288 header_fn = os.path.join(self.output, 'clar_suite.h')
289
290 with io.StringIO() as header_file:
291 header_file.write(u"#ifndef _____clar_suite_h_____\n")
292 header_file.write(u"#define _____clar_suite_h_____\n")
293
294 modules = sorted(self.modules.values(), key=lambda module: module.name)
295
296 for module in modules:
297 t = Module.DeclarationTemplate(module)
298 header_file.write(t.render())
299
300 header_file.write(u"#endif\n")
301
302 return self.write_output(header_fn, header_file.getvalue())
303
304 return False
305
306 if __name__ == '__main__':
307 from optparse import OptionParser
308
309 parser = OptionParser()
310 parser.add_option('-f', '--force', action="store_true", dest='force', default=False)
311 parser.add_option('-x', '--exclude', dest='excluded', action='append', default=[])
312 parser.add_option('-o', '--output', dest='output')
313
314 options, args = parser.parse_args()
315 if len(args) > 1:
316 print("More than one path given")
317 sys.exit(1)
318
319 path = args.pop() if args else '.'
320 if os.path.isfile(path) and not options.output:
321 print("Must provide --output when specifying a file")
322 sys.exit(1)
323 output = options.output or path
324
325 suite = TestSuite(path, output)
326 suite.load(options.force)
327 suite.disable(options.excluded)
328 if suite.write():
329 print("Written `clar.suite`, `clar_suite.h` (%d tests in %d suites)" % (suite.callback_count(), suite.suite_count()))