]> git.proxmox.com Git - libgit2.git/blame - tests/clar/generate.py
Merge https://salsa.debian.org/debian/libgit2 into proxmox/bullseye
[libgit2.git] / tests / clar / generate.py
CommitLineData
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
9from __future__ import with_statement
10from string import Template
e579e0f7 11import re, fnmatch, os, sys, codecs, pickle, io
2e6f06a8
VM
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:
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 144class 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
296if __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