]>
Commit | Line | Data |
---|---|---|
2e6f06a8 | 1 | #!/usr/bin/env python |
e8a92fe1 RB |
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 | # | |
2e6f06a8 VM |
8 | |
9 | from __future__ import with_statement | |
10 | from string import Template | |
e579e0f7 | 11 | import re, fnmatch, os, sys, codecs, pickle, io |
2e6f06a8 VM |
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: | |
e8a92fe1 RB |
20 | return ' { NULL, NULL }' |
21 | return ' { "%s", &%s }' % (cb['short_name'], cb['symbol']) | |
2e6f06a8 VM |
22 | |
23 | class DeclarationTemplate(Template): | |
24 | def render(self): | |
e8a92fe1 | 25 | out = "\n".join("extern %s;" % cb['declaration'] for cb in self.module.callbacks) + "\n" |
2e6f06a8 | 26 | |
22a2d3d5 UG |
27 | for initializer in self.module.initializers: |
28 | out += "extern %s;\n" % initializer['declaration'] | |
2e6f06a8 VM |
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): | |
22a2d3d5 UG |
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( | |
e8a92fe1 RB |
57 | r""" |
58 | { | |
59 | "${clean_name}", | |
60 | ${initialize}, | |
61 | ${cleanup}, | |
62 | ${cb_ptr}, ${cb_count}, ${enabled} | |
63 | }""" | |
22a2d3d5 UG |
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) | |
2e6f06a8 VM |
75 | |
76 | def __init__(self, name): | |
77 | self.name = name | |
7202ec29 RB |
78 | |
79 | self.mtime = 0 | |
2e6f06a8 | 80 | self.enabled = True |
7202ec29 | 81 | self.modified = False |
2e6f06a8 VM |
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): | |
3a1eb9e5 | 98 | TEST_FUNC_REGEX = r"^(void\s+(test_%s__(\w+))\s*\(\s*void\s*\))\s*\{" |
2e6f06a8 VM |
99 | |
100 | contents = self._skip_comments(contents) | |
101 | regex = re.compile(TEST_FUNC_REGEX % self.name, re.MULTILINE) | |
102 | ||
103 | self.callbacks = [] | |
22a2d3d5 | 104 | self.initializers = [] |
2e6f06a8 VM |
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 | ||
22a2d3d5 UG |
114 | if short_name.startswith('initialize'): |
115 | self.initializers.append(data) | |
2e6f06a8 VM |
116 | elif short_name == 'cleanup': |
117 | self.cleanup = data | |
118 | else: | |
119 | self.callbacks.append(data) | |
120 | ||
121 | return self.callbacks != [] | |
122 | ||
7202ec29 RB |
123 | def refresh(self, path): |
124 | self.modified = False | |
125 | ||
2e6f06a8 | 126 | try: |
7202ec29 RB |
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 | ||
3a1eb9e5 | 136 | with codecs.open(path, encoding='utf-8') as fp: |
7202ec29 RB |
137 | raw_content = fp.read() |
138 | ||
2e6f06a8 VM |
139 | except IOError: |
140 | return False | |
141 | ||
7202ec29 RB |
142 | return self.parse(raw_content) |
143 | ||
2e6f06a8 | 144 | class TestSuite(object): |
7202ec29 | 145 | |
eae0bfdc | 146 | def __init__(self, path, output): |
2e6f06a8 | 147 | self.path = path |
eae0bfdc | 148 | self.output = output |
2e6f06a8 | 149 | |
e579e0f7 | 150 | def maybe_generate(self, path): |
7202ec29 RB |
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 | ||
2e6f06a8 VM |
159 | def find_modules(self): |
160 | modules = [] | |
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] | |
164 | ||
165 | tests_in_module = fnmatch.filter(files, "*.c") | |
166 | ||
167 | for test_file in tests_in_module: | |
168 | full_path = os.path.join(root, test_file) | |
3a1eb9e5 | 169 | module_name = "_".join(module_root + [test_file[:-2]]).replace("-", "_") |
2e6f06a8 VM |
170 | |
171 | modules.append((full_path, module_name)) | |
172 | ||
173 | return modules | |
174 | ||
7202ec29 | 175 | def load_cache(self): |
eae0bfdc | 176 | path = os.path.join(self.output, '.clarcache') |
7202ec29 RB |
177 | cache = {} |
178 | ||
179 | try: | |
180 | fp = open(path, 'rb') | |
181 | cache = pickle.load(fp) | |
182 | fp.close() | |
183 | except (IOError, ValueError): | |
184 | pass | |
185 | ||
186 | return cache | |
187 | ||
188 | def save_cache(self): | |
eae0bfdc | 189 | path = os.path.join(self.output, '.clarcache') |
7202ec29 RB |
190 | with open(path, 'wb') as cache: |
191 | pickle.dump(self.modules, cache) | |
192 | ||
2e6f06a8 VM |
193 | def load(self, force = False): |
194 | module_data = self.find_modules() | |
7202ec29 | 195 | self.modules = {} if force else self.load_cache() |
2e6f06a8 VM |
196 | |
197 | for path, name in module_data: | |
198 | if name not in self.modules: | |
199 | self.modules[name] = Module(name) | |
200 | ||
7202ec29 | 201 | if not self.modules[name].refresh(path): |
2e6f06a8 VM |
202 | del self.modules[name] |
203 | ||
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 | |
211 | ||
212 | def suite_count(self): | |
22a2d3d5 | 213 | return sum(max(1, len(m.initializers)) for m in self.modules.values()) |
2e6f06a8 VM |
214 | |
215 | def callback_count(self): | |
216 | return sum(len(module.callbacks) for module in self.modules.values()) | |
217 | ||
218 | def write(self): | |
e579e0f7 MB |
219 | wrote_suite = self.write_suite() |
220 | wrote_header = self.write_header() | |
2e6f06a8 | 221 | |
e579e0f7 MB |
222 | if wrote_suite or wrote_header: |
223 | self.save_cache() | |
224 | return True | |
225 | ||
226 | return False | |
227 | ||
228 | def write_output(self, fn, data): | |
229 | if not self.maybe_generate(fn): | |
7202ec29 RB |
230 | return False |
231 | ||
e579e0f7 MB |
232 | current = None |
233 | ||
234 | try: | |
235 | with open(fn, 'r') as input: | |
236 | current = input.read() | |
237 | except OSError: | |
238 | pass | |
239 | except IOError: | |
240 | pass | |
241 | ||
242 | if current == data: | |
243 | return False | |
244 | ||
245 | with open(fn, 'w') as output: | |
246 | output.write(data) | |
247 | ||
248 | return True | |
249 | ||
250 | def write_suite(self): | |
251 | suite_fn = os.path.join(self.output, 'clar.suite') | |
252 | ||
253 | with io.StringIO() as suite_file: | |
eae0bfdc PP |
254 | modules = sorted(self.modules.values(), key=lambda module: module.name) |
255 | ||
256 | for module in modules: | |
2e6f06a8 | 257 | t = Module.DeclarationTemplate(module) |
e579e0f7 | 258 | suite_file.write(t.render()) |
2e6f06a8 | 259 | |
eae0bfdc | 260 | for module in modules: |
2e6f06a8 | 261 | t = Module.CallbacksTemplate(module) |
e579e0f7 | 262 | suite_file.write(t.render()) |
2e6f06a8 VM |
263 | |
264 | suites = "static struct clar_suite _clar_suites[] = {" + ','.join( | |
eae0bfdc | 265 | Module.InfoTemplate(module).render() for module in modules |
e8a92fe1 | 266 | ) + "\n};\n" |
2e6f06a8 | 267 | |
e579e0f7 | 268 | suite_file.write(suites) |
2e6f06a8 | 269 | |
e579e0f7 MB |
270 | suite_file.write(u"static const size_t _clar_suite_count = %d;\n" % self.suite_count()) |
271 | suite_file.write(u"static const size_t _clar_callback_count = %d;\n" % self.callback_count()) | |
2e6f06a8 | 272 | |
e579e0f7 MB |
273 | return self.write_output(suite_fn, suite_file.getvalue()) |
274 | ||
275 | return False | |
276 | ||
277 | def write_header(self): | |
278 | header_fn = os.path.join(self.output, 'clar_suite.h') | |
279 | ||
280 | with io.StringIO() as header_file: | |
281 | header_file.write(u"#ifndef _____clar_suite_h_____\n") | |
282 | header_file.write(u"#define _____clar_suite_h_____\n") | |
283 | ||
284 | modules = sorted(self.modules.values(), key=lambda module: module.name) | |
285 | ||
286 | for module in modules: | |
287 | t = Module.DeclarationTemplate(module) | |
288 | header_file.write(t.render()) | |
289 | ||
290 | header_file.write(u"#endif\n") | |
291 | ||
292 | return self.write_output(header_fn, header_file.getvalue()) | |
293 | ||
294 | return False | |
7202ec29 | 295 | |
2e6f06a8 VM |
296 | if __name__ == '__main__': |
297 | from optparse import OptionParser | |
298 | ||
299 | parser = OptionParser() | |
3a1eb9e5 | 300 | parser.add_option('-f', '--force', action="store_true", dest='force', default=False) |
2e6f06a8 | 301 | parser.add_option('-x', '--exclude', dest='excluded', action='append', default=[]) |
eae0bfdc | 302 | parser.add_option('-o', '--output', dest='output') |
2e6f06a8 VM |
303 | |
304 | options, args = parser.parse_args() | |
eae0bfdc PP |
305 | if len(args) > 1: |
306 | print("More than one path given") | |
307 | sys.exit(1) | |
308 | ||
309 | path = args.pop() if args else '.' | |
310 | output = options.output or path | |
311 | suite = TestSuite(path, output) | |
312 | suite.load(options.force) | |
313 | suite.disable(options.excluded) | |
314 | if suite.write(): | |
e579e0f7 | 315 | print("Written `clar.suite`, `clar_suite.h` (%d tests in %d suites)" % (suite.callback_count(), suite.suite_count())) |
2e6f06a8 | 316 |