OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 |
| 3 # This Source Code Form is subject to the terms of the Mozilla Public |
| 4 # License, v. 2.0. If a copy of the MPL was not distributed with this file, |
| 5 # You can obtain one at http://mozilla.org/MPL/2.0/. |
| 6 |
| 7 """ |
| 8 Mozilla universal manifest parser |
| 9 """ |
| 10 |
| 11 __all__ = ['read_ini', # .ini reader |
| 12 'ManifestParser', 'TestManifest', 'convert', # manifest handling |
| 13 'parse', 'ParseError', 'ExpressionParser'] # conditional expression p
arser |
| 14 |
| 15 import os |
| 16 import re |
| 17 import shutil |
| 18 import sys |
| 19 from fnmatch import fnmatch |
| 20 from optparse import OptionParser |
| 21 |
| 22 # we need relpath, but it is introduced in python 2.6 |
| 23 # http://docs.python.org/library/os.path.html |
| 24 try: |
| 25 relpath = os.path.relpath |
| 26 except AttributeError: |
| 27 def relpath(path, start): |
| 28 """ |
| 29 Return a relative version of a path |
| 30 from /usr/lib/python2.6/posixpath.py |
| 31 """ |
| 32 |
| 33 if not path: |
| 34 raise ValueError("no path specified") |
| 35 |
| 36 start_list = os.path.abspath(start).split(os.path.sep) |
| 37 path_list = os.path.abspath(path).split(os.path.sep) |
| 38 |
| 39 # Work out how much of the filepath is shared by start and path. |
| 40 i = len(os.path.commonprefix([start_list, path_list])) |
| 41 |
| 42 rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:] |
| 43 if not rel_list: |
| 44 return os.curdir |
| 45 return os.path.join(*rel_list) |
| 46 |
| 47 # expr.py |
| 48 # from: |
| 49 # http://k0s.org/mozilla/hg/expressionparser |
| 50 # http://hg.mozilla.org/users/tmielczarek_mozilla.com/expressionparser |
| 51 |
| 52 # Implements a top-down parser/evaluator for simple boolean expressions. |
| 53 # ideas taken from http://effbot.org/zone/simple-top-down-parsing.htm |
| 54 # |
| 55 # Rough grammar: |
| 56 # expr := literal |
| 57 # | '(' expr ')' |
| 58 # | expr '&&' expr |
| 59 # | expr '||' expr |
| 60 # | expr '==' expr |
| 61 # | expr '!=' expr |
| 62 # literal := BOOL |
| 63 # | INT |
| 64 # | STRING |
| 65 # | IDENT |
| 66 # BOOL := true|false |
| 67 # INT := [0-9]+ |
| 68 # STRING := "[^"]*" |
| 69 # IDENT := [A-Za-z_]\w* |
| 70 |
| 71 # Identifiers take their values from a mapping dictionary passed as the second |
| 72 # argument. |
| 73 |
| 74 # Glossary (see above URL for details): |
| 75 # - nud: null denotation |
| 76 # - led: left detonation |
| 77 # - lbp: left binding power |
| 78 # - rbp: right binding power |
| 79 |
| 80 class ident_token(object): |
| 81 def __init__(self, value): |
| 82 self.value = value |
| 83 def nud(self, parser): |
| 84 # identifiers take their value from the value mappings passed |
| 85 # to the parser |
| 86 return parser.value(self.value) |
| 87 |
| 88 class literal_token(object): |
| 89 def __init__(self, value): |
| 90 self.value = value |
| 91 def nud(self, parser): |
| 92 return self.value |
| 93 |
| 94 class eq_op_token(object): |
| 95 "==" |
| 96 def led(self, parser, left): |
| 97 return left == parser.expression(self.lbp) |
| 98 |
| 99 class neq_op_token(object): |
| 100 "!=" |
| 101 def led(self, parser, left): |
| 102 return left != parser.expression(self.lbp) |
| 103 |
| 104 class not_op_token(object): |
| 105 "!" |
| 106 def nud(self, parser): |
| 107 return not parser.expression() |
| 108 |
| 109 class and_op_token(object): |
| 110 "&&" |
| 111 def led(self, parser, left): |
| 112 right = parser.expression(self.lbp) |
| 113 return left and right |
| 114 |
| 115 class or_op_token(object): |
| 116 "||" |
| 117 def led(self, parser, left): |
| 118 right = parser.expression(self.lbp) |
| 119 return left or right |
| 120 |
| 121 class lparen_token(object): |
| 122 "(" |
| 123 def nud(self, parser): |
| 124 expr = parser.expression() |
| 125 parser.advance(rparen_token) |
| 126 return expr |
| 127 |
| 128 class rparen_token(object): |
| 129 ")" |
| 130 |
| 131 class end_token(object): |
| 132 """always ends parsing""" |
| 133 |
| 134 ### derived literal tokens |
| 135 |
| 136 class bool_token(literal_token): |
| 137 def __init__(self, value): |
| 138 value = {'true':True, 'false':False}[value] |
| 139 literal_token.__init__(self, value) |
| 140 |
| 141 class int_token(literal_token): |
| 142 def __init__(self, value): |
| 143 literal_token.__init__(self, int(value)) |
| 144 |
| 145 class string_token(literal_token): |
| 146 def __init__(self, value): |
| 147 literal_token.__init__(self, value[1:-1]) |
| 148 |
| 149 precedence = [(end_token, rparen_token), |
| 150 (or_op_token,), |
| 151 (and_op_token,), |
| 152 (eq_op_token, neq_op_token), |
| 153 (lparen_token,), |
| 154 ] |
| 155 for index, rank in enumerate(precedence): |
| 156 for token in rank: |
| 157 token.lbp = index # lbp = lowest left binding power |
| 158 |
| 159 class ParseError(Exception): |
| 160 """errror parsing conditional expression""" |
| 161 |
| 162 class ExpressionParser(object): |
| 163 def __init__(self, text, valuemapping, strict=False): |
| 164 """ |
| 165 Initialize the parser with input |text|, and |valuemapping| as |
| 166 a dict mapping identifier names to values. |
| 167 """ |
| 168 self.text = text |
| 169 self.valuemapping = valuemapping |
| 170 self.strict = strict |
| 171 |
| 172 def _tokenize(self): |
| 173 """ |
| 174 Lex the input text into tokens and yield them in sequence. |
| 175 """ |
| 176 # scanner callbacks |
| 177 def bool_(scanner, t): return bool_token(t) |
| 178 def identifier(scanner, t): return ident_token(t) |
| 179 def integer(scanner, t): return int_token(t) |
| 180 def eq(scanner, t): return eq_op_token() |
| 181 def neq(scanner, t): return neq_op_token() |
| 182 def or_(scanner, t): return or_op_token() |
| 183 def and_(scanner, t): return and_op_token() |
| 184 def lparen(scanner, t): return lparen_token() |
| 185 def rparen(scanner, t): return rparen_token() |
| 186 def string_(scanner, t): return string_token(t) |
| 187 def not_(scanner, t): return not_op_token() |
| 188 |
| 189 scanner = re.Scanner([ |
| 190 (r"true|false", bool_), |
| 191 (r"[a-zA-Z_]\w*", identifier), |
| 192 (r"[0-9]+", integer), |
| 193 (r'("[^"]*")|(\'[^\']*\')', string_), |
| 194 (r"==", eq), |
| 195 (r"!=", neq), |
| 196 (r"\|\|", or_), |
| 197 (r"!", not_), |
| 198 (r"&&", and_), |
| 199 (r"\(", lparen), |
| 200 (r"\)", rparen), |
| 201 (r"\s+", None), # skip whitespace |
| 202 ]) |
| 203 tokens, remainder = scanner.scan(self.text) |
| 204 for t in tokens: |
| 205 yield t |
| 206 yield end_token() |
| 207 |
| 208 def value(self, ident): |
| 209 """ |
| 210 Look up the value of |ident| in the value mapping passed in the |
| 211 constructor. |
| 212 """ |
| 213 if self.strict: |
| 214 return self.valuemapping[ident] |
| 215 else: |
| 216 return self.valuemapping.get(ident, None) |
| 217 |
| 218 def advance(self, expected): |
| 219 """ |
| 220 Assert that the next token is an instance of |expected|, and advance |
| 221 to the next token. |
| 222 """ |
| 223 if not isinstance(self.token, expected): |
| 224 raise Exception, "Unexpected token!" |
| 225 self.token = self.iter.next() |
| 226 |
| 227 def expression(self, rbp=0): |
| 228 """ |
| 229 Parse and return the value of an expression until a token with |
| 230 right binding power greater than rbp is encountered. |
| 231 """ |
| 232 t = self.token |
| 233 self.token = self.iter.next() |
| 234 left = t.nud(self) |
| 235 while rbp < self.token.lbp: |
| 236 t = self.token |
| 237 self.token = self.iter.next() |
| 238 left = t.led(self, left) |
| 239 return left |
| 240 |
| 241 def parse(self): |
| 242 """ |
| 243 Parse and return the value of the expression in the text |
| 244 passed to the constructor. Raises a ParseError if the expression |
| 245 could not be parsed. |
| 246 """ |
| 247 try: |
| 248 self.iter = self._tokenize() |
| 249 self.token = self.iter.next() |
| 250 return self.expression() |
| 251 except: |
| 252 raise ParseError("could not parse: %s; variables: %s" % (self.text,
self.valuemapping)) |
| 253 |
| 254 __call__ = parse |
| 255 |
| 256 def parse(text, **values): |
| 257 """ |
| 258 Parse and evaluate a boolean expression in |text|. Use |values| to look |
| 259 up the value of identifiers referenced in the expression. Returns the final |
| 260 value of the expression. A ParseError will be raised if parsing fails. |
| 261 """ |
| 262 return ExpressionParser(text, values).parse() |
| 263 |
| 264 def normalize_path(path): |
| 265 """normalize a relative path""" |
| 266 if sys.platform.startswith('win'): |
| 267 return path.replace('/', os.path.sep) |
| 268 return path |
| 269 |
| 270 def denormalize_path(path): |
| 271 """denormalize a relative path""" |
| 272 if sys.platform.startswith('win'): |
| 273 return path.replace(os.path.sep, '/') |
| 274 return path |
| 275 |
| 276 |
| 277 def read_ini(fp, variables=None, default='DEFAULT', |
| 278 comments=';#', separators=('=', ':'), |
| 279 strict=True): |
| 280 """ |
| 281 read an .ini file and return a list of [(section, values)] |
| 282 - fp : file pointer or path to read |
| 283 - variables : default set of variables |
| 284 - default : name of the section for the default section |
| 285 - comments : characters that if they start a line denote a comment |
| 286 - separators : strings that denote key, value separation in order |
| 287 - strict : whether to be strict about parsing |
| 288 """ |
| 289 |
| 290 if variables is None: |
| 291 variables = {} |
| 292 |
| 293 if isinstance(fp, basestring): |
| 294 fp = file(fp) |
| 295 |
| 296 sections = [] |
| 297 key = value = None |
| 298 section_names = set([]) |
| 299 |
| 300 # read the lines |
| 301 for line in fp.readlines(): |
| 302 |
| 303 stripped = line.strip() |
| 304 |
| 305 # ignore blank lines |
| 306 if not stripped: |
| 307 # reset key and value to avoid continuation lines |
| 308 key = value = None |
| 309 continue |
| 310 |
| 311 # ignore comment lines |
| 312 if stripped[0] in comments: |
| 313 continue |
| 314 |
| 315 # check for a new section |
| 316 if len(stripped) > 2 and stripped[0] == '[' and stripped[-1] == ']': |
| 317 section = stripped[1:-1].strip() |
| 318 key = value = None |
| 319 |
| 320 # deal with DEFAULT section |
| 321 if section.lower() == default.lower(): |
| 322 if strict: |
| 323 assert default not in section_names |
| 324 section_names.add(default) |
| 325 current_section = variables |
| 326 continue |
| 327 |
| 328 if strict: |
| 329 # make sure this section doesn't already exist |
| 330 assert section not in section_names, "Section '%s' already found
in '%s'" % (section, section_names) |
| 331 |
| 332 section_names.add(section) |
| 333 current_section = {} |
| 334 sections.append((section, current_section)) |
| 335 continue |
| 336 |
| 337 # if there aren't any sections yet, something bad happen |
| 338 if not section_names: |
| 339 raise Exception('No sections found') |
| 340 |
| 341 # (key, value) pair |
| 342 for separator in separators: |
| 343 if separator in stripped: |
| 344 key, value = stripped.split(separator, 1) |
| 345 key = key.strip() |
| 346 value = value.strip() |
| 347 |
| 348 if strict: |
| 349 # make sure this key isn't already in the section or empty |
| 350 assert key |
| 351 if current_section is not variables: |
| 352 assert key not in current_section |
| 353 |
| 354 current_section[key] = value |
| 355 break |
| 356 else: |
| 357 # continuation line ? |
| 358 if line[0].isspace() and key: |
| 359 value = '%s%s%s' % (value, os.linesep, stripped) |
| 360 current_section[key] = value |
| 361 else: |
| 362 # something bad happen! |
| 363 raise Exception("Not sure what you're trying to do") |
| 364 |
| 365 # interpret the variables |
| 366 def interpret_variables(global_dict, local_dict): |
| 367 variables = global_dict.copy() |
| 368 variables.update(local_dict) |
| 369 return variables |
| 370 |
| 371 sections = [(i, interpret_variables(variables, j)) for i, j in sections] |
| 372 return sections |
| 373 |
| 374 |
| 375 ### objects for parsing manifests |
| 376 |
| 377 class ManifestParser(object): |
| 378 """read .ini manifests""" |
| 379 |
| 380 ### methods for reading manifests |
| 381 |
| 382 def __init__(self, manifests=(), defaults=None, strict=True): |
| 383 self._defaults = defaults or {} |
| 384 self.tests = [] |
| 385 self.strict = strict |
| 386 self.rootdir = None |
| 387 self.relativeRoot = None |
| 388 if manifests: |
| 389 self.read(*manifests) |
| 390 |
| 391 def getRelativeRoot(self, root): |
| 392 return root |
| 393 |
| 394 def _read(self, root, filename, defaults): |
| 395 |
| 396 # get directory of this file |
| 397 here = os.path.dirname(os.path.abspath(filename)) |
| 398 defaults['here'] = here |
| 399 |
| 400 # read the configuration |
| 401 sections = read_ini(fp=filename, variables=defaults, strict=self.strict) |
| 402 |
| 403 # get the tests |
| 404 for section, data in sections: |
| 405 |
| 406 # a file to include |
| 407 # TODO: keep track of included file structure: |
| 408 # self.manifests = {'manifest.ini': 'relative/path.ini'} |
| 409 if section.startswith('include:'): |
| 410 include_file = section.split('include:', 1)[-1] |
| 411 include_file = normalize_path(include_file) |
| 412 if not os.path.isabs(include_file): |
| 413 include_file = os.path.join(self.getRelativeRoot(here), incl
ude_file) |
| 414 if not os.path.exists(include_file): |
| 415 if self.strict: |
| 416 raise IOError("File '%s' does not exist" % include_file) |
| 417 else: |
| 418 continue |
| 419 include_defaults = data.copy() |
| 420 self._read(root, include_file, include_defaults) |
| 421 continue |
| 422 |
| 423 # otherwise an item |
| 424 test = data |
| 425 test['name'] = section |
| 426 test['manifest'] = os.path.abspath(filename) |
| 427 |
| 428 # determine the path |
| 429 path = test.get('path', section) |
| 430 _relpath = path |
| 431 if '://' not in path: # don't futz with URLs |
| 432 path = normalize_path(path) |
| 433 if not os.path.isabs(path): |
| 434 path = os.path.join(here, path) |
| 435 _relpath = relpath(path, self.rootdir) |
| 436 |
| 437 test['path'] = path |
| 438 test['relpath'] = _relpath |
| 439 |
| 440 # append the item |
| 441 self.tests.append(test) |
| 442 |
| 443 def read(self, *filenames, **defaults): |
| 444 |
| 445 # ensure all files exist |
| 446 missing = [ filename for filename in filenames |
| 447 if not os.path.exists(filename) ] |
| 448 if missing: |
| 449 raise IOError('Missing files: %s' % ', '.join(missing)) |
| 450 |
| 451 # process each file |
| 452 for filename in filenames: |
| 453 |
| 454 # set the per file defaults |
| 455 defaults = defaults.copy() or self._defaults.copy() |
| 456 here = os.path.dirname(os.path.abspath(filename)) |
| 457 defaults['here'] = here |
| 458 |
| 459 if self.rootdir is None: |
| 460 # set the root directory |
| 461 # == the directory of the first manifest given |
| 462 self.rootdir = here |
| 463 |
| 464 self._read(here, filename, defaults) |
| 465 |
| 466 ### methods for querying manifests |
| 467 |
| 468 def query(self, *checks, **kw): |
| 469 """ |
| 470 general query function for tests |
| 471 - checks : callable conditions to test if the test fulfills the query |
| 472 """ |
| 473 tests = kw.get('tests', None) |
| 474 if tests is None: |
| 475 tests = self.tests |
| 476 retval = [] |
| 477 for test in tests: |
| 478 for check in checks: |
| 479 if not check(test): |
| 480 break |
| 481 else: |
| 482 retval.append(test) |
| 483 return retval |
| 484 |
| 485 def get(self, _key=None, inverse=False, tags=None, tests=None, **kwargs): |
| 486 # TODO: pass a dict instead of kwargs since you might hav |
| 487 # e.g. 'inverse' as a key in the dict |
| 488 |
| 489 # TODO: tags should just be part of kwargs with None values |
| 490 # (None == any is kinda weird, but probably still better) |
| 491 |
| 492 # fix up tags |
| 493 if tags: |
| 494 tags = set(tags) |
| 495 else: |
| 496 tags = set() |
| 497 |
| 498 # make some check functions |
| 499 if inverse: |
| 500 has_tags = lambda test: not tags.intersection(test.keys()) |
| 501 def dict_query(test): |
| 502 for key, value in kwargs.items(): |
| 503 if test.get(key) == value: |
| 504 return False |
| 505 return True |
| 506 else: |
| 507 has_tags = lambda test: tags.issubset(test.keys()) |
| 508 def dict_query(test): |
| 509 for key, value in kwargs.items(): |
| 510 if test.get(key) != value: |
| 511 return False |
| 512 return True |
| 513 |
| 514 # query the tests |
| 515 tests = self.query(has_tags, dict_query, tests=tests) |
| 516 |
| 517 # if a key is given, return only a list of that key |
| 518 # useful for keys like 'name' or 'path' |
| 519 if _key: |
| 520 return [test[_key] for test in tests] |
| 521 |
| 522 # return the tests |
| 523 return tests |
| 524 |
| 525 def missing(self, tests=None): |
| 526 """return list of tests that do not exist on the filesystem""" |
| 527 if tests is None: |
| 528 tests = self.tests |
| 529 return [test for test in tests |
| 530 if not os.path.exists(test['path'])] |
| 531 |
| 532 def manifests(self, tests=None): |
| 533 """ |
| 534 return manifests in order in which they appear in the tests |
| 535 """ |
| 536 if tests is None: |
| 537 tests = self.tests |
| 538 manifests = [] |
| 539 for test in tests: |
| 540 manifest = test.get('manifest') |
| 541 if not manifest: |
| 542 continue |
| 543 if manifest not in manifests: |
| 544 manifests.append(manifest) |
| 545 return manifests |
| 546 |
| 547 ### methods for outputting from manifests |
| 548 |
| 549 def write(self, fp=sys.stdout, rootdir=None, |
| 550 global_tags=None, global_kwargs=None, |
| 551 local_tags=None, local_kwargs=None): |
| 552 """ |
| 553 write a manifest given a query |
| 554 global and local options will be munged to do the query |
| 555 globals will be written to the top of the file |
| 556 locals (if given) will be written per test |
| 557 """ |
| 558 |
| 559 # root directory |
| 560 if rootdir is None: |
| 561 rootdir = self.rootdir |
| 562 |
| 563 # sanitize input |
| 564 global_tags = global_tags or set() |
| 565 local_tags = local_tags or set() |
| 566 global_kwargs = global_kwargs or {} |
| 567 local_kwargs = local_kwargs or {} |
| 568 |
| 569 # create the query |
| 570 tags = set([]) |
| 571 tags.update(global_tags) |
| 572 tags.update(local_tags) |
| 573 kwargs = {} |
| 574 kwargs.update(global_kwargs) |
| 575 kwargs.update(local_kwargs) |
| 576 |
| 577 # get matching tests |
| 578 tests = self.get(tags=tags, **kwargs) |
| 579 |
| 580 # print the .ini manifest |
| 581 if global_tags or global_kwargs: |
| 582 print >> fp, '[DEFAULT]' |
| 583 for tag in global_tags: |
| 584 print >> fp, '%s =' % tag |
| 585 for key, value in global_kwargs.items(): |
| 586 print >> fp, '%s = %s' % (key, value) |
| 587 print >> fp |
| 588 |
| 589 for test in tests: |
| 590 test = test.copy() # don't overwrite |
| 591 |
| 592 path = test['name'] |
| 593 if not os.path.isabs(path): |
| 594 path = test['path'] |
| 595 if self.rootdir: |
| 596 path = relpath(test['path'], self.rootdir) |
| 597 path = denormalize_path(path) |
| 598 print >> fp, '[%s]' % path |
| 599 |
| 600 # reserved keywords: |
| 601 reserved = ['path', 'name', 'here', 'manifest', 'relpath'] |
| 602 for key in sorted(test.keys()): |
| 603 if key in reserved: |
| 604 continue |
| 605 if key in global_kwargs: |
| 606 continue |
| 607 if key in global_tags and not test[key]: |
| 608 continue |
| 609 print >> fp, '%s = %s' % (key, test[key]) |
| 610 print >> fp |
| 611 |
| 612 def copy(self, directory, rootdir=None, *tags, **kwargs): |
| 613 """ |
| 614 copy the manifests and associated tests |
| 615 - directory : directory to copy to |
| 616 - rootdir : root directory to copy to (if not given from manifests) |
| 617 - tags : keywords the tests must have |
| 618 - kwargs : key, values the tests must match |
| 619 """ |
| 620 # XXX note that copy does *not* filter the tests out of the |
| 621 # resulting manifest; it just stupidly copies them over. |
| 622 # ideally, it would reread the manifests and filter out the |
| 623 # tests that don't match *tags and **kwargs |
| 624 |
| 625 # destination |
| 626 if not os.path.exists(directory): |
| 627 os.path.makedirs(directory) |
| 628 else: |
| 629 # sanity check |
| 630 assert os.path.isdir(directory) |
| 631 |
| 632 # tests to copy |
| 633 tests = self.get(tags=tags, **kwargs) |
| 634 if not tests: |
| 635 return # nothing to do! |
| 636 |
| 637 # root directory |
| 638 if rootdir is None: |
| 639 rootdir = self.rootdir |
| 640 |
| 641 # copy the manifests + tests |
| 642 manifests = [relpath(manifest, rootdir) for manifest in self.manifests()
] |
| 643 for manifest in manifests: |
| 644 destination = os.path.join(directory, manifest) |
| 645 dirname = os.path.dirname(destination) |
| 646 if not os.path.exists(dirname): |
| 647 os.makedirs(dirname) |
| 648 else: |
| 649 # sanity check |
| 650 assert os.path.isdir(dirname) |
| 651 shutil.copy(os.path.join(rootdir, manifest), destination) |
| 652 for test in tests: |
| 653 if os.path.isabs(test['name']): |
| 654 continue |
| 655 source = test['path'] |
| 656 if not os.path.exists(source): |
| 657 print >> sys.stderr, "Missing test: '%s' does not exist!" % sour
ce |
| 658 continue |
| 659 # TODO: should err on strict |
| 660 destination = os.path.join(directory, relpath(test['path'], rootdir)
) |
| 661 shutil.copy(source, destination) |
| 662 # TODO: ensure that all of the tests are below the from_dir |
| 663 |
| 664 def update(self, from_dir, rootdir=None, *tags, **kwargs): |
| 665 """ |
| 666 update the tests as listed in a manifest from a directory |
| 667 - from_dir : directory where the tests live |
| 668 - rootdir : root directory to copy to (if not given from manifests) |
| 669 - tags : keys the tests must have |
| 670 - kwargs : key, values the tests must match |
| 671 """ |
| 672 |
| 673 # get the tests |
| 674 tests = self.get(tags=tags, **kwargs) |
| 675 |
| 676 # get the root directory |
| 677 if not rootdir: |
| 678 rootdir = self.rootdir |
| 679 |
| 680 # copy them! |
| 681 for test in tests: |
| 682 if not os.path.isabs(test['name']): |
| 683 _relpath = relpath(test['path'], rootdir) |
| 684 source = os.path.join(from_dir, _relpath) |
| 685 if not os.path.exists(source): |
| 686 # TODO err on strict |
| 687 print >> sys.stderr, "Missing test: '%s'; skipping" % test['
name'] |
| 688 continue |
| 689 destination = os.path.join(rootdir, _relpath) |
| 690 shutil.copy(source, destination) |
| 691 |
| 692 |
| 693 class TestManifest(ManifestParser): |
| 694 """ |
| 695 apply logic to manifests; this is your integration layer :) |
| 696 specific harnesses may subclass from this if they need more logic |
| 697 """ |
| 698 |
| 699 def filter(self, values, tests): |
| 700 """ |
| 701 filter on a specific list tag, e.g.: |
| 702 run-if = os == win linux |
| 703 skip-if = os == mac |
| 704 """ |
| 705 |
| 706 # tags: |
| 707 run_tag = 'run-if' |
| 708 skip_tag = 'skip-if' |
| 709 fail_tag = 'fail-if' |
| 710 |
| 711 # loop over test |
| 712 for test in tests: |
| 713 reason = None # reason to disable |
| 714 |
| 715 # tagged-values to run |
| 716 if run_tag in test: |
| 717 condition = test[run_tag] |
| 718 if not parse(condition, **values): |
| 719 reason = '%s: %s' % (run_tag, condition) |
| 720 |
| 721 # tagged-values to skip |
| 722 if skip_tag in test: |
| 723 condition = test[skip_tag] |
| 724 if parse(condition, **values): |
| 725 reason = '%s: %s' % (skip_tag, condition) |
| 726 |
| 727 # mark test as disabled if there's a reason |
| 728 if reason: |
| 729 test.setdefault('disabled', reason) |
| 730 |
| 731 # mark test as a fail if so indicated |
| 732 if fail_tag in test: |
| 733 condition = test[fail_tag] |
| 734 if parse(condition, **values): |
| 735 test['expected'] = 'fail' |
| 736 |
| 737 def active_tests(self, exists=True, disabled=True, **values): |
| 738 """ |
| 739 - exists : return only existing tests |
| 740 - disabled : whether to return disabled tests |
| 741 - tags : keys and values to filter on (e.g. `os = linux mac`) |
| 742 """ |
| 743 |
| 744 tests = [i.copy() for i in self.tests] # shallow copy |
| 745 |
| 746 # mark all tests as passing unless indicated otherwise |
| 747 for test in tests: |
| 748 test['expected'] = test.get('expected', 'pass') |
| 749 |
| 750 # ignore tests that do not exist |
| 751 if exists: |
| 752 tests = [test for test in tests if os.path.exists(test['path'])] |
| 753 |
| 754 # filter by tags |
| 755 self.filter(values, tests) |
| 756 |
| 757 # ignore disabled tests if specified |
| 758 if not disabled: |
| 759 tests = [test for test in tests |
| 760 if not 'disabled' in test] |
| 761 |
| 762 # return active tests |
| 763 return tests |
| 764 |
| 765 def test_paths(self): |
| 766 return [test['path'] for test in self.active_tests()] |
| 767 |
| 768 |
| 769 ### utility function(s); probably belongs elsewhere |
| 770 |
| 771 def convert(directories, pattern=None, ignore=(), write=None): |
| 772 """ |
| 773 convert directories to a simple manifest |
| 774 """ |
| 775 |
| 776 retval = [] |
| 777 include = [] |
| 778 for directory in directories: |
| 779 for dirpath, dirnames, filenames in os.walk(directory): |
| 780 |
| 781 # filter out directory names |
| 782 dirnames = [ i for i in dirnames if i not in ignore ] |
| 783 dirnames.sort() |
| 784 |
| 785 # reference only the subdirectory |
| 786 _dirpath = dirpath |
| 787 dirpath = dirpath.split(directory, 1)[-1].strip(os.path.sep) |
| 788 |
| 789 if dirpath.split(os.path.sep)[0] in ignore: |
| 790 continue |
| 791 |
| 792 # filter by glob |
| 793 if pattern: |
| 794 filenames = [filename for filename in filenames |
| 795 if fnmatch(filename, pattern)] |
| 796 |
| 797 filenames.sort() |
| 798 |
| 799 # write a manifest for each directory |
| 800 if write and (dirnames or filenames): |
| 801 manifest = file(os.path.join(_dirpath, write), 'w') |
| 802 for dirname in dirnames: |
| 803 print >> manifest, '[include:%s]' % os.path.join(dirname, wr
ite) |
| 804 for filename in filenames: |
| 805 print >> manifest, '[%s]' % filename |
| 806 manifest.close() |
| 807 |
| 808 # add to the list |
| 809 retval.extend([denormalize_path(os.path.join(dirpath, filename)) |
| 810 for filename in filenames]) |
| 811 |
| 812 if write: |
| 813 return # the manifests have already been written! |
| 814 |
| 815 retval.sort() |
| 816 retval = ['[%s]' % filename for filename in retval] |
| 817 return '\n'.join(retval) |
| 818 |
| 819 ### command line attributes |
| 820 |
| 821 class ParserError(Exception): |
| 822 """error for exceptions while parsing the command line""" |
| 823 |
| 824 def parse_args(_args): |
| 825 """ |
| 826 parse and return: |
| 827 --keys=value (or --key value) |
| 828 -tags |
| 829 args |
| 830 """ |
| 831 |
| 832 # return values |
| 833 _dict = {} |
| 834 tags = [] |
| 835 args = [] |
| 836 |
| 837 # parse the arguments |
| 838 key = None |
| 839 for arg in _args: |
| 840 if arg.startswith('---'): |
| 841 raise ParserError("arguments should start with '-' or '--' only") |
| 842 elif arg.startswith('--'): |
| 843 if key: |
| 844 raise ParserError("Key %s still open" % key) |
| 845 key = arg[2:] |
| 846 if '=' in key: |
| 847 key, value = key.split('=', 1) |
| 848 _dict[key] = value |
| 849 key = None |
| 850 continue |
| 851 elif arg.startswith('-'): |
| 852 if key: |
| 853 raise ParserError("Key %s still open" % key) |
| 854 tags.append(arg[1:]) |
| 855 continue |
| 856 else: |
| 857 if key: |
| 858 _dict[key] = arg |
| 859 continue |
| 860 args.append(arg) |
| 861 |
| 862 # return values |
| 863 return (_dict, tags, args) |
| 864 |
| 865 |
| 866 ### classes for subcommands |
| 867 |
| 868 class CLICommand(object): |
| 869 usage = '%prog [options] command' |
| 870 def __init__(self, parser): |
| 871 self._parser = parser # master parser |
| 872 def parser(self): |
| 873 return OptionParser(usage=self.usage, description=self.__doc__, |
| 874 add_help_option=False) |
| 875 |
| 876 class Copy(CLICommand): |
| 877 usage = '%prog [options] copy manifest directory -tag1 -tag2 --key1=value1 -
-key2=value2 ...' |
| 878 def __call__(self, options, args): |
| 879 # parse the arguments |
| 880 try: |
| 881 kwargs, tags, args = parse_args(args) |
| 882 except ParserError, e: |
| 883 self._parser.error(e.message) |
| 884 |
| 885 # make sure we have some manifests, otherwise it will |
| 886 # be quite boring |
| 887 if not len(args) == 2: |
| 888 HelpCLI(self._parser)(options, ['copy']) |
| 889 return |
| 890 |
| 891 # read the manifests |
| 892 # TODO: should probably ensure these exist here |
| 893 manifests = ManifestParser() |
| 894 manifests.read(args[0]) |
| 895 |
| 896 # print the resultant query |
| 897 manifests.copy(args[1], None, *tags, **kwargs) |
| 898 |
| 899 |
| 900 class CreateCLI(CLICommand): |
| 901 """ |
| 902 create a manifest from a list of directories |
| 903 """ |
| 904 usage = '%prog [options] create directory <directory> <...>' |
| 905 |
| 906 def parser(self): |
| 907 parser = CLICommand.parser(self) |
| 908 parser.add_option('-p', '--pattern', dest='pattern', |
| 909 help="glob pattern for files") |
| 910 parser.add_option('-i', '--ignore', dest='ignore', |
| 911 default=[], action='append', |
| 912 help='directories to ignore') |
| 913 parser.add_option('-w', '--in-place', dest='in_place', |
| 914 help='Write .ini files in place; filename to write to'
) |
| 915 return parser |
| 916 |
| 917 def __call__(self, _options, args): |
| 918 parser = self.parser() |
| 919 options, args = parser.parse_args(args) |
| 920 |
| 921 # need some directories |
| 922 if not len(args): |
| 923 parser.print_usage() |
| 924 return |
| 925 |
| 926 # add the directories to the manifest |
| 927 for arg in args: |
| 928 assert os.path.exists(arg) |
| 929 assert os.path.isdir(arg) |
| 930 manifest = convert(args, pattern=options.pattern, ignore=options.ign
ore, |
| 931 write=options.in_place) |
| 932 if manifest: |
| 933 print manifest |
| 934 |
| 935 |
| 936 class WriteCLI(CLICommand): |
| 937 """ |
| 938 write a manifest based on a query |
| 939 """ |
| 940 usage = '%prog [options] write manifest <manifest> -tag1 -tag2 --key1=value1
--key2=value2 ...' |
| 941 def __call__(self, options, args): |
| 942 |
| 943 # parse the arguments |
| 944 try: |
| 945 kwargs, tags, args = parse_args(args) |
| 946 except ParserError, e: |
| 947 self._parser.error(e.message) |
| 948 |
| 949 # make sure we have some manifests, otherwise it will |
| 950 # be quite boring |
| 951 if not args: |
| 952 HelpCLI(self._parser)(options, ['write']) |
| 953 return |
| 954 |
| 955 # read the manifests |
| 956 # TODO: should probably ensure these exist here |
| 957 manifests = ManifestParser() |
| 958 manifests.read(*args) |
| 959 |
| 960 # print the resultant query |
| 961 manifests.write(global_tags=tags, global_kwargs=kwargs) |
| 962 |
| 963 |
| 964 class HelpCLI(CLICommand): |
| 965 """ |
| 966 get help on a command |
| 967 """ |
| 968 usage = '%prog [options] help [command]' |
| 969 |
| 970 def __call__(self, options, args): |
| 971 if len(args) == 1 and args[0] in commands: |
| 972 commands[args[0]](self._parser).parser().print_help() |
| 973 else: |
| 974 self._parser.print_help() |
| 975 print '\nCommands:' |
| 976 for command in sorted(commands): |
| 977 print ' %s : %s' % (command, commands[command].__doc__.strip()) |
| 978 |
| 979 class UpdateCLI(CLICommand): |
| 980 """ |
| 981 update the tests as listed in a manifest from a directory |
| 982 """ |
| 983 usage = '%prog [options] update manifest directory -tag1 -tag2 --key1=value1
--key2=value2 ...' |
| 984 |
| 985 def __call__(self, options, args): |
| 986 # parse the arguments |
| 987 try: |
| 988 kwargs, tags, args = parse_args(args) |
| 989 except ParserError, e: |
| 990 self._parser.error(e.message) |
| 991 |
| 992 # make sure we have some manifests, otherwise it will |
| 993 # be quite boring |
| 994 if not len(args) == 2: |
| 995 HelpCLI(self._parser)(options, ['update']) |
| 996 return |
| 997 |
| 998 # read the manifests |
| 999 # TODO: should probably ensure these exist here |
| 1000 manifests = ManifestParser() |
| 1001 manifests.read(args[0]) |
| 1002 |
| 1003 # print the resultant query |
| 1004 manifests.update(args[1], None, *tags, **kwargs) |
| 1005 |
| 1006 |
| 1007 # command -> class mapping |
| 1008 commands = { 'create': CreateCLI, |
| 1009 'help': HelpCLI, |
| 1010 'update': UpdateCLI, |
| 1011 'write': WriteCLI } |
| 1012 |
| 1013 def main(args=sys.argv[1:]): |
| 1014 """console_script entry point""" |
| 1015 |
| 1016 # set up an option parser |
| 1017 usage = '%prog [options] [command] ...' |
| 1018 description = "%s. Use `help` to display commands" % __doc__.strip() |
| 1019 parser = OptionParser(usage=usage, description=description) |
| 1020 parser.add_option('-s', '--strict', dest='strict', |
| 1021 action='store_true', default=False, |
| 1022 help='adhere strictly to errors') |
| 1023 parser.disable_interspersed_args() |
| 1024 |
| 1025 options, args = parser.parse_args(args) |
| 1026 |
| 1027 if not args: |
| 1028 HelpCLI(parser)(options, args) |
| 1029 parser.exit() |
| 1030 |
| 1031 # get the command |
| 1032 command = args[0] |
| 1033 if command not in commands: |
| 1034 parser.error("Command must be one of %s (you gave '%s')" % (', '.join(so
rted(commands.keys())), command)) |
| 1035 |
| 1036 handler = commands[command](parser) |
| 1037 handler(options, args[1:]) |
| 1038 |
| 1039 if __name__ == '__main__': |
| 1040 main() |
OLD | NEW |