Git fork
at reftables-rust 278 lines 8.8 kB view 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 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()))