Git fork
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
9from __future__ import with_statement
10from string import Template
11import re, fnmatch, os, sys, codecs, pickle
12
13class 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
144class TestSuite(object):
145
146 def __init__(self, path, output):
147 self.path = path
148 self.output = output
149
150 def should_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 output = os.path.join(self.output, 'clar.suite')
227 os.makedirs(self.output, exist_ok=True)
228
229 if not self.should_generate(output):
230 return False
231
232 with open(output, 'w') as data:
233 modules = sorted(self.modules.values(), key=lambda module: module.name)
234
235 for module in modules:
236 t = Module.DeclarationTemplate(module)
237 data.write(t.render())
238
239 for module in modules:
240 t = Module.CallbacksTemplate(module)
241 data.write(t.render())
242
243 suites = "static struct clar_suite _clar_suites[] = {" + ','.join(
244 Module.InfoTemplate(module).render() for module in modules
245 ) + "\n};\n"
246
247 data.write(suites)
248
249 data.write("static const size_t _clar_suite_count = %d;\n" % self.suite_count())
250 data.write("static const size_t _clar_callback_count = %d;\n" % self.callback_count())
251
252 self.save_cache()
253 return True
254
255if __name__ == '__main__':
256 from optparse import OptionParser
257
258 parser = OptionParser()
259 parser.add_option('-f', '--force', action="store_true", dest='force', default=False)
260 parser.add_option('-x', '--exclude', dest='excluded', action='append', default=[])
261 parser.add_option('-o', '--output', dest='output')
262
263 options, args = parser.parse_args()
264 if len(args) > 1:
265 print("More than one path given")
266 sys.exit(1)
267
268 path = args.pop() if args else '.'
269 if os.path.isfile(path) and not options.output:
270 print("Must provide --output when specifying a file")
271 sys.exit(1)
272 output = options.output or path
273
274 suite = TestSuite(path, output)
275 suite.load(options.force)
276 suite.disable(options.excluded)
277 if suite.write():
278 print("Written `clar.suite` (%d tests in %d suites)" % (suite.callback_count(), suite.suite_count()))