]>
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 | |
11 | import re, fnmatch, os, codecs, pickle | |
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 VM |
26 | |
27 | if self.module.initialize: | |
28 | out += "extern %s;\n" % self.module.initialize['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 | return Template( | |
e8a92fe1 RB |
45 | r""" |
46 | { | |
47 | "${clean_name}", | |
48 | ${initialize}, | |
49 | ${cleanup}, | |
50 | ${cb_ptr}, ${cb_count}, ${enabled} | |
51 | }""" | |
2e6f06a8 VM |
52 | ).substitute( |
53 | clean_name = self.module.clean_name(), | |
54 | initialize = self._render_callback(self.module.initialize), | |
55 | cleanup = self._render_callback(self.module.cleanup), | |
56 | cb_ptr = "_clar_cb_%s" % self.module.name, | |
57 | cb_count = len(self.module.callbacks), | |
58 | enabled = int(self.module.enabled) | |
59 | ) | |
60 | ||
61 | def __init__(self, name): | |
62 | self.name = name | |
7202ec29 RB |
63 | |
64 | self.mtime = 0 | |
2e6f06a8 | 65 | self.enabled = True |
7202ec29 | 66 | self.modified = False |
2e6f06a8 VM |
67 | |
68 | def clean_name(self): | |
69 | return self.name.replace("_", "::") | |
70 | ||
71 | def _skip_comments(self, text): | |
72 | SKIP_COMMENTS_REGEX = re.compile( | |
73 | r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', | |
74 | re.DOTALL | re.MULTILINE) | |
75 | ||
76 | def _replacer(match): | |
77 | s = match.group(0) | |
78 | return "" if s.startswith('/') else s | |
79 | ||
80 | return re.sub(SKIP_COMMENTS_REGEX, _replacer, text) | |
81 | ||
82 | def parse(self, contents): | |
3a1eb9e5 | 83 | TEST_FUNC_REGEX = r"^(void\s+(test_%s__(\w+))\s*\(\s*void\s*\))\s*\{" |
2e6f06a8 VM |
84 | |
85 | contents = self._skip_comments(contents) | |
86 | regex = re.compile(TEST_FUNC_REGEX % self.name, re.MULTILINE) | |
87 | ||
88 | self.callbacks = [] | |
89 | self.initialize = None | |
90 | self.cleanup = None | |
91 | ||
92 | for (declaration, symbol, short_name) in regex.findall(contents): | |
93 | data = { | |
94 | "short_name" : short_name, | |
95 | "declaration" : declaration, | |
96 | "symbol" : symbol | |
97 | } | |
98 | ||
99 | if short_name == 'initialize': | |
100 | self.initialize = data | |
101 | elif short_name == 'cleanup': | |
102 | self.cleanup = data | |
103 | else: | |
104 | self.callbacks.append(data) | |
105 | ||
106 | return self.callbacks != [] | |
107 | ||
7202ec29 RB |
108 | def refresh(self, path): |
109 | self.modified = False | |
110 | ||
2e6f06a8 | 111 | try: |
7202ec29 RB |
112 | st = os.stat(path) |
113 | ||
114 | # Not modified | |
115 | if st.st_mtime == self.mtime: | |
116 | return True | |
117 | ||
118 | self.modified = True | |
119 | self.mtime = st.st_mtime | |
120 | ||
3a1eb9e5 | 121 | with codecs.open(path, encoding='utf-8') as fp: |
7202ec29 RB |
122 | raw_content = fp.read() |
123 | ||
2e6f06a8 VM |
124 | except IOError: |
125 | return False | |
126 | ||
7202ec29 RB |
127 | return self.parse(raw_content) |
128 | ||
2e6f06a8 | 129 | class TestSuite(object): |
7202ec29 | 130 | |
2e6f06a8 VM |
131 | def __init__(self, path): |
132 | self.path = path | |
133 | ||
7202ec29 RB |
134 | def should_generate(self, path): |
135 | if not os.path.isfile(path): | |
136 | return True | |
137 | ||
138 | if any(module.modified for module in self.modules.values()): | |
139 | return True | |
140 | ||
141 | return False | |
142 | ||
2e6f06a8 VM |
143 | def find_modules(self): |
144 | modules = [] | |
145 | for root, _, files in os.walk(self.path): | |
146 | module_root = root[len(self.path):] | |
147 | module_root = [c for c in module_root.split(os.sep) if c] | |
148 | ||
149 | tests_in_module = fnmatch.filter(files, "*.c") | |
150 | ||
151 | for test_file in tests_in_module: | |
152 | full_path = os.path.join(root, test_file) | |
3a1eb9e5 | 153 | module_name = "_".join(module_root + [test_file[:-2]]).replace("-", "_") |
2e6f06a8 VM |
154 | |
155 | modules.append((full_path, module_name)) | |
156 | ||
157 | return modules | |
158 | ||
7202ec29 RB |
159 | def load_cache(self): |
160 | path = os.path.join(self.path, '.clarcache') | |
161 | cache = {} | |
162 | ||
163 | try: | |
164 | fp = open(path, 'rb') | |
165 | cache = pickle.load(fp) | |
166 | fp.close() | |
167 | except (IOError, ValueError): | |
168 | pass | |
169 | ||
170 | return cache | |
171 | ||
172 | def save_cache(self): | |
173 | path = os.path.join(self.path, '.clarcache') | |
174 | with open(path, 'wb') as cache: | |
175 | pickle.dump(self.modules, cache) | |
176 | ||
2e6f06a8 VM |
177 | def load(self, force = False): |
178 | module_data = self.find_modules() | |
7202ec29 | 179 | self.modules = {} if force else self.load_cache() |
2e6f06a8 VM |
180 | |
181 | for path, name in module_data: | |
182 | if name not in self.modules: | |
183 | self.modules[name] = Module(name) | |
184 | ||
7202ec29 | 185 | if not self.modules[name].refresh(path): |
2e6f06a8 VM |
186 | del self.modules[name] |
187 | ||
188 | def disable(self, excluded): | |
189 | for exclude in excluded: | |
190 | for module in self.modules.values(): | |
191 | name = module.clean_name() | |
192 | if name.startswith(exclude): | |
193 | module.enabled = False | |
194 | module.modified = True | |
195 | ||
196 | def suite_count(self): | |
197 | return len(self.modules) | |
198 | ||
199 | def callback_count(self): | |
200 | return sum(len(module.callbacks) for module in self.modules.values()) | |
201 | ||
202 | def write(self): | |
203 | output = os.path.join(self.path, 'clar.suite') | |
204 | ||
7202ec29 RB |
205 | if not self.should_generate(output): |
206 | return False | |
207 | ||
2e6f06a8 VM |
208 | with open(output, 'w') as data: |
209 | for module in self.modules.values(): | |
210 | t = Module.DeclarationTemplate(module) | |
211 | data.write(t.render()) | |
212 | ||
213 | for module in self.modules.values(): | |
214 | t = Module.CallbacksTemplate(module) | |
215 | data.write(t.render()) | |
216 | ||
217 | suites = "static struct clar_suite _clar_suites[] = {" + ','.join( | |
e8a92fe1 RB |
218 | Module.InfoTemplate(module).render() for module in sorted(self.modules.values(), key=lambda module: module.name) |
219 | ) + "\n};\n" | |
2e6f06a8 VM |
220 | |
221 | data.write(suites) | |
222 | ||
e8a92fe1 RB |
223 | data.write("static const size_t _clar_suite_count = %d;\n" % self.suite_count()) |
224 | data.write("static const size_t _clar_callback_count = %d;\n" % self.callback_count()) | |
2e6f06a8 | 225 | |
3a1eb9e5 | 226 | self.save_cache() |
7202ec29 RB |
227 | return True |
228 | ||
2e6f06a8 VM |
229 | if __name__ == '__main__': |
230 | from optparse import OptionParser | |
231 | ||
232 | parser = OptionParser() | |
3a1eb9e5 | 233 | parser.add_option('-f', '--force', action="store_true", dest='force', default=False) |
2e6f06a8 VM |
234 | parser.add_option('-x', '--exclude', dest='excluded', action='append', default=[]) |
235 | ||
236 | options, args = parser.parse_args() | |
237 | ||
238 | for path in args or ['.']: | |
239 | suite = TestSuite(path) | |
7202ec29 | 240 | suite.load(options.force) |
2e6f06a8 | 241 | suite.disable(options.excluded) |
7202ec29 RB |
242 | if suite.write(): |
243 | print("Written `clar.suite` (%d tests in %d suites)" % (suite.callback_count(), suite.suite_count())) | |
2e6f06a8 | 244 |