testing/mozbase/manifestdestiny/manifestparser/manifestparser.py

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rwxr-xr-x

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 #!/usr/bin/env python
michael@0 2
michael@0 3 # This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 5 # You can obtain one at http://mozilla.org/MPL/2.0/.
michael@0 6
michael@0 7 """
michael@0 8 Mozilla universal manifest parser
michael@0 9 """
michael@0 10
michael@0 11 __all__ = ['read_ini', # .ini reader
michael@0 12 'ManifestParser', 'TestManifest', 'convert', # manifest handling
michael@0 13 'parse', 'ParseError', 'ExpressionParser'] # conditional expression parser
michael@0 14
michael@0 15 import fnmatch
michael@0 16 import os
michael@0 17 import re
michael@0 18 import shutil
michael@0 19 import sys
michael@0 20
michael@0 21 from optparse import OptionParser
michael@0 22 from StringIO import StringIO
michael@0 23
michael@0 24 relpath = os.path.relpath
michael@0 25 string = (basestring,)
michael@0 26
michael@0 27
michael@0 28 # expr.py
michael@0 29 # from:
michael@0 30 # http://k0s.org/mozilla/hg/expressionparser
michael@0 31 # http://hg.mozilla.org/users/tmielczarek_mozilla.com/expressionparser
michael@0 32
michael@0 33 # Implements a top-down parser/evaluator for simple boolean expressions.
michael@0 34 # ideas taken from http://effbot.org/zone/simple-top-down-parsing.htm
michael@0 35 #
michael@0 36 # Rough grammar:
michael@0 37 # expr := literal
michael@0 38 # | '(' expr ')'
michael@0 39 # | expr '&&' expr
michael@0 40 # | expr '||' expr
michael@0 41 # | expr '==' expr
michael@0 42 # | expr '!=' expr
michael@0 43 # literal := BOOL
michael@0 44 # | INT
michael@0 45 # | STRING
michael@0 46 # | IDENT
michael@0 47 # BOOL := true|false
michael@0 48 # INT := [0-9]+
michael@0 49 # STRING := "[^"]*"
michael@0 50 # IDENT := [A-Za-z_]\w*
michael@0 51
michael@0 52 # Identifiers take their values from a mapping dictionary passed as the second
michael@0 53 # argument.
michael@0 54
michael@0 55 # Glossary (see above URL for details):
michael@0 56 # - nud: null denotation
michael@0 57 # - led: left detonation
michael@0 58 # - lbp: left binding power
michael@0 59 # - rbp: right binding power
michael@0 60
michael@0 61 class ident_token(object):
michael@0 62 def __init__(self, value):
michael@0 63 self.value = value
michael@0 64 def nud(self, parser):
michael@0 65 # identifiers take their value from the value mappings passed
michael@0 66 # to the parser
michael@0 67 return parser.value(self.value)
michael@0 68
michael@0 69 class literal_token(object):
michael@0 70 def __init__(self, value):
michael@0 71 self.value = value
michael@0 72 def nud(self, parser):
michael@0 73 return self.value
michael@0 74
michael@0 75 class eq_op_token(object):
michael@0 76 "=="
michael@0 77 def led(self, parser, left):
michael@0 78 return left == parser.expression(self.lbp)
michael@0 79
michael@0 80 class neq_op_token(object):
michael@0 81 "!="
michael@0 82 def led(self, parser, left):
michael@0 83 return left != parser.expression(self.lbp)
michael@0 84
michael@0 85 class not_op_token(object):
michael@0 86 "!"
michael@0 87 def nud(self, parser):
michael@0 88 return not parser.expression(100)
michael@0 89
michael@0 90 class and_op_token(object):
michael@0 91 "&&"
michael@0 92 def led(self, parser, left):
michael@0 93 right = parser.expression(self.lbp)
michael@0 94 return left and right
michael@0 95
michael@0 96 class or_op_token(object):
michael@0 97 "||"
michael@0 98 def led(self, parser, left):
michael@0 99 right = parser.expression(self.lbp)
michael@0 100 return left or right
michael@0 101
michael@0 102 class lparen_token(object):
michael@0 103 "("
michael@0 104 def nud(self, parser):
michael@0 105 expr = parser.expression()
michael@0 106 parser.advance(rparen_token)
michael@0 107 return expr
michael@0 108
michael@0 109 class rparen_token(object):
michael@0 110 ")"
michael@0 111
michael@0 112 class end_token(object):
michael@0 113 """always ends parsing"""
michael@0 114
michael@0 115 ### derived literal tokens
michael@0 116
michael@0 117 class bool_token(literal_token):
michael@0 118 def __init__(self, value):
michael@0 119 value = {'true':True, 'false':False}[value]
michael@0 120 literal_token.__init__(self, value)
michael@0 121
michael@0 122 class int_token(literal_token):
michael@0 123 def __init__(self, value):
michael@0 124 literal_token.__init__(self, int(value))
michael@0 125
michael@0 126 class string_token(literal_token):
michael@0 127 def __init__(self, value):
michael@0 128 literal_token.__init__(self, value[1:-1])
michael@0 129
michael@0 130 precedence = [(end_token, rparen_token),
michael@0 131 (or_op_token,),
michael@0 132 (and_op_token,),
michael@0 133 (eq_op_token, neq_op_token),
michael@0 134 (lparen_token,),
michael@0 135 ]
michael@0 136 for index, rank in enumerate(precedence):
michael@0 137 for token in rank:
michael@0 138 token.lbp = index # lbp = lowest left binding power
michael@0 139
michael@0 140 class ParseError(Exception):
michael@0 141 """error parsing conditional expression"""
michael@0 142
michael@0 143 class ExpressionParser(object):
michael@0 144 """
michael@0 145 A parser for a simple expression language.
michael@0 146
michael@0 147 The expression language can be described as follows::
michael@0 148
michael@0 149 EXPRESSION ::= LITERAL | '(' EXPRESSION ')' | '!' EXPRESSION | EXPRESSION OP EXPRESSION
michael@0 150 OP ::= '==' | '!=' | '&&' | '||'
michael@0 151 LITERAL ::= BOOL | INT | IDENT | STRING
michael@0 152 BOOL ::= 'true' | 'false'
michael@0 153 INT ::= [0-9]+
michael@0 154 IDENT ::= [a-zA-Z_]\w*
michael@0 155 STRING ::= '"' [^\"] '"' | ''' [^\'] '''
michael@0 156
michael@0 157 At its core, expressions consist of booleans, integers, identifiers and.
michael@0 158 strings. Booleans are one of *true* or *false*. Integers are a series
michael@0 159 of digits. Identifiers are a series of English letters and underscores.
michael@0 160 Strings are a pair of matching quote characters (single or double) with
michael@0 161 zero or more characters inside.
michael@0 162
michael@0 163 Expressions can be combined with operators: the equals (==) and not
michael@0 164 equals (!=) operators compare two expressions and produce a boolean. The
michael@0 165 and (&&) and or (||) operators take two expressions and produce the logical
michael@0 166 AND or OR value of them, respectively. An expression can also be prefixed
michael@0 167 with the not (!) operator, which produces its logical negation.
michael@0 168
michael@0 169 Finally, any expression may be contained within parentheses for grouping.
michael@0 170
michael@0 171 Identifiers take their values from the mapping provided.
michael@0 172 """
michael@0 173 def __init__(self, text, valuemapping, strict=False):
michael@0 174 """
michael@0 175 Initialize the parser
michael@0 176 :param text: The expression to parse as a string.
michael@0 177 :param valuemapping: A dict mapping identifier names to values.
michael@0 178 :param strict: If true, referencing an identifier that was not
michael@0 179 provided in :valuemapping: will raise an error.
michael@0 180 """
michael@0 181 self.text = text
michael@0 182 self.valuemapping = valuemapping
michael@0 183 self.strict = strict
michael@0 184
michael@0 185 def _tokenize(self):
michael@0 186 """
michael@0 187 Lex the input text into tokens and yield them in sequence.
michael@0 188 """
michael@0 189 # scanner callbacks
michael@0 190 def bool_(scanner, t): return bool_token(t)
michael@0 191 def identifier(scanner, t): return ident_token(t)
michael@0 192 def integer(scanner, t): return int_token(t)
michael@0 193 def eq(scanner, t): return eq_op_token()
michael@0 194 def neq(scanner, t): return neq_op_token()
michael@0 195 def or_(scanner, t): return or_op_token()
michael@0 196 def and_(scanner, t): return and_op_token()
michael@0 197 def lparen(scanner, t): return lparen_token()
michael@0 198 def rparen(scanner, t): return rparen_token()
michael@0 199 def string_(scanner, t): return string_token(t)
michael@0 200 def not_(scanner, t): return not_op_token()
michael@0 201
michael@0 202 scanner = re.Scanner([
michael@0 203 # Note: keep these in sync with the class docstring above.
michael@0 204 (r"true|false", bool_),
michael@0 205 (r"[a-zA-Z_]\w*", identifier),
michael@0 206 (r"[0-9]+", integer),
michael@0 207 (r'("[^"]*")|(\'[^\']*\')', string_),
michael@0 208 (r"==", eq),
michael@0 209 (r"!=", neq),
michael@0 210 (r"\|\|", or_),
michael@0 211 (r"!", not_),
michael@0 212 (r"&&", and_),
michael@0 213 (r"\(", lparen),
michael@0 214 (r"\)", rparen),
michael@0 215 (r"\s+", None), # skip whitespace
michael@0 216 ])
michael@0 217 tokens, remainder = scanner.scan(self.text)
michael@0 218 for t in tokens:
michael@0 219 yield t
michael@0 220 yield end_token()
michael@0 221
michael@0 222 def value(self, ident):
michael@0 223 """
michael@0 224 Look up the value of |ident| in the value mapping passed in the
michael@0 225 constructor.
michael@0 226 """
michael@0 227 if self.strict:
michael@0 228 return self.valuemapping[ident]
michael@0 229 else:
michael@0 230 return self.valuemapping.get(ident, None)
michael@0 231
michael@0 232 def advance(self, expected):
michael@0 233 """
michael@0 234 Assert that the next token is an instance of |expected|, and advance
michael@0 235 to the next token.
michael@0 236 """
michael@0 237 if not isinstance(self.token, expected):
michael@0 238 raise Exception, "Unexpected token!"
michael@0 239 self.token = self.iter.next()
michael@0 240
michael@0 241 def expression(self, rbp=0):
michael@0 242 """
michael@0 243 Parse and return the value of an expression until a token with
michael@0 244 right binding power greater than rbp is encountered.
michael@0 245 """
michael@0 246 t = self.token
michael@0 247 self.token = self.iter.next()
michael@0 248 left = t.nud(self)
michael@0 249 while rbp < self.token.lbp:
michael@0 250 t = self.token
michael@0 251 self.token = self.iter.next()
michael@0 252 left = t.led(self, left)
michael@0 253 return left
michael@0 254
michael@0 255 def parse(self):
michael@0 256 """
michael@0 257 Parse and return the value of the expression in the text
michael@0 258 passed to the constructor. Raises a ParseError if the expression
michael@0 259 could not be parsed.
michael@0 260 """
michael@0 261 try:
michael@0 262 self.iter = self._tokenize()
michael@0 263 self.token = self.iter.next()
michael@0 264 return self.expression()
michael@0 265 except:
michael@0 266 raise ParseError("could not parse: %s; variables: %s" % (self.text, self.valuemapping))
michael@0 267
michael@0 268 __call__ = parse
michael@0 269
michael@0 270 def parse(text, **values):
michael@0 271 """
michael@0 272 Parse and evaluate a boolean expression.
michael@0 273 :param text: The expression to parse, as a string.
michael@0 274 :param values: A dict containing a name to value mapping for identifiers
michael@0 275 referenced in *text*.
michael@0 276 :rtype: the final value of the expression.
michael@0 277 :raises: :py:exc::ParseError: will be raised if parsing fails.
michael@0 278 """
michael@0 279 return ExpressionParser(text, values).parse()
michael@0 280
michael@0 281
michael@0 282 ### path normalization
michael@0 283
michael@0 284 def normalize_path(path):
michael@0 285 """normalize a relative path"""
michael@0 286 if sys.platform.startswith('win'):
michael@0 287 return path.replace('/', os.path.sep)
michael@0 288 return path
michael@0 289
michael@0 290 def denormalize_path(path):
michael@0 291 """denormalize a relative path"""
michael@0 292 if sys.platform.startswith('win'):
michael@0 293 return path.replace(os.path.sep, '/')
michael@0 294 return path
michael@0 295
michael@0 296
michael@0 297 ### .ini reader
michael@0 298
michael@0 299 def read_ini(fp, variables=None, default='DEFAULT',
michael@0 300 comments=';#', separators=('=', ':'),
michael@0 301 strict=True):
michael@0 302 """
michael@0 303 read an .ini file and return a list of [(section, values)]
michael@0 304 - fp : file pointer or path to read
michael@0 305 - variables : default set of variables
michael@0 306 - default : name of the section for the default section
michael@0 307 - comments : characters that if they start a line denote a comment
michael@0 308 - separators : strings that denote key, value separation in order
michael@0 309 - strict : whether to be strict about parsing
michael@0 310 """
michael@0 311
michael@0 312 # variables
michael@0 313 variables = variables or {}
michael@0 314 sections = []
michael@0 315 key = value = None
michael@0 316 section_names = set()
michael@0 317 if isinstance(fp, basestring):
michael@0 318 fp = file(fp)
michael@0 319
michael@0 320 # read the lines
michael@0 321 for (linenum, line) in enumerate(fp.readlines(), start=1):
michael@0 322
michael@0 323 stripped = line.strip()
michael@0 324
michael@0 325 # ignore blank lines
michael@0 326 if not stripped:
michael@0 327 # reset key and value to avoid continuation lines
michael@0 328 key = value = None
michael@0 329 continue
michael@0 330
michael@0 331 # ignore comment lines
michael@0 332 if stripped[0] in comments:
michael@0 333 continue
michael@0 334
michael@0 335 # check for a new section
michael@0 336 if len(stripped) > 2 and stripped[0] == '[' and stripped[-1] == ']':
michael@0 337 section = stripped[1:-1].strip()
michael@0 338 key = value = None
michael@0 339
michael@0 340 # deal with DEFAULT section
michael@0 341 if section.lower() == default.lower():
michael@0 342 if strict:
michael@0 343 assert default not in section_names
michael@0 344 section_names.add(default)
michael@0 345 current_section = variables
michael@0 346 continue
michael@0 347
michael@0 348 if strict:
michael@0 349 # make sure this section doesn't already exist
michael@0 350 assert section not in section_names, "Section '%s' already found in '%s'" % (section, section_names)
michael@0 351
michael@0 352 section_names.add(section)
michael@0 353 current_section = {}
michael@0 354 sections.append((section, current_section))
michael@0 355 continue
michael@0 356
michael@0 357 # if there aren't any sections yet, something bad happen
michael@0 358 if not section_names:
michael@0 359 raise Exception('No sections found')
michael@0 360
michael@0 361 # (key, value) pair
michael@0 362 for separator in separators:
michael@0 363 if separator in stripped:
michael@0 364 key, value = stripped.split(separator, 1)
michael@0 365 key = key.strip()
michael@0 366 value = value.strip()
michael@0 367
michael@0 368 if strict:
michael@0 369 # make sure this key isn't already in the section or empty
michael@0 370 assert key
michael@0 371 if current_section is not variables:
michael@0 372 assert key not in current_section
michael@0 373
michael@0 374 current_section[key] = value
michael@0 375 break
michael@0 376 else:
michael@0 377 # continuation line ?
michael@0 378 if line[0].isspace() and key:
michael@0 379 value = '%s%s%s' % (value, os.linesep, stripped)
michael@0 380 current_section[key] = value
michael@0 381 else:
michael@0 382 # something bad happened!
michael@0 383 if hasattr(fp, 'name'):
michael@0 384 filename = fp.name
michael@0 385 else:
michael@0 386 filename = 'unknown'
michael@0 387 raise Exception("Error parsing manifest file '%s', line %s" %
michael@0 388 (filename, linenum))
michael@0 389
michael@0 390 # interpret the variables
michael@0 391 def interpret_variables(global_dict, local_dict):
michael@0 392 variables = global_dict.copy()
michael@0 393 if 'skip-if' in local_dict and 'skip-if' in variables:
michael@0 394 local_dict['skip-if'] = "(%s) || (%s)" % (variables['skip-if'].split('#')[0], local_dict['skip-if'].split('#')[0])
michael@0 395 variables.update(local_dict)
michael@0 396
michael@0 397 return variables
michael@0 398
michael@0 399 sections = [(i, interpret_variables(variables, j)) for i, j in sections]
michael@0 400 return sections
michael@0 401
michael@0 402
michael@0 403 ### objects for parsing manifests
michael@0 404
michael@0 405 class ManifestParser(object):
michael@0 406 """read .ini manifests"""
michael@0 407
michael@0 408 def __init__(self, manifests=(), defaults=None, strict=True):
michael@0 409 self._defaults = defaults or {}
michael@0 410 self.tests = []
michael@0 411 self.manifest_defaults = {}
michael@0 412 self.strict = strict
michael@0 413 self.rootdir = None
michael@0 414 self.relativeRoot = None
michael@0 415 if manifests:
michael@0 416 self.read(*manifests)
michael@0 417
michael@0 418 def getRelativeRoot(self, root):
michael@0 419 return root
michael@0 420
michael@0 421 ### methods for reading manifests
michael@0 422
michael@0 423 def _read(self, root, filename, defaults):
michael@0 424
michael@0 425 # get directory of this file if not file-like object
michael@0 426 if isinstance(filename, string):
michael@0 427 filename = os.path.abspath(filename)
michael@0 428 fp = open(filename)
michael@0 429 here = os.path.dirname(filename)
michael@0 430 else:
michael@0 431 fp = filename
michael@0 432 filename = here = None
michael@0 433 defaults['here'] = here
michael@0 434
michael@0 435 # Rootdir is needed for relative path calculation. Precompute it for
michael@0 436 # the microoptimization used below.
michael@0 437 if self.rootdir is None:
michael@0 438 rootdir = ""
michael@0 439 else:
michael@0 440 assert os.path.isabs(self.rootdir)
michael@0 441 rootdir = self.rootdir + os.path.sep
michael@0 442
michael@0 443 # read the configuration
michael@0 444 sections = read_ini(fp=fp, variables=defaults, strict=self.strict)
michael@0 445 self.manifest_defaults[filename] = defaults
michael@0 446
michael@0 447 # get the tests
michael@0 448 for section, data in sections:
michael@0 449 subsuite = ''
michael@0 450 if 'subsuite' in data:
michael@0 451 subsuite = data['subsuite']
michael@0 452
michael@0 453 # a file to include
michael@0 454 # TODO: keep track of included file structure:
michael@0 455 # self.manifests = {'manifest.ini': 'relative/path.ini'}
michael@0 456 if section.startswith('include:'):
michael@0 457 include_file = section.split('include:', 1)[-1]
michael@0 458 include_file = normalize_path(include_file)
michael@0 459 if not os.path.isabs(include_file):
michael@0 460 include_file = os.path.join(self.getRelativeRoot(here), include_file)
michael@0 461 if not os.path.exists(include_file):
michael@0 462 message = "Included file '%s' does not exist" % include_file
michael@0 463 if self.strict:
michael@0 464 raise IOError(message)
michael@0 465 else:
michael@0 466 sys.stderr.write("%s\n" % message)
michael@0 467 continue
michael@0 468 include_defaults = data.copy()
michael@0 469 self._read(root, include_file, include_defaults)
michael@0 470 continue
michael@0 471
michael@0 472 # otherwise an item
michael@0 473 test = data
michael@0 474 test['name'] = section
michael@0 475
michael@0 476 # Will be None if the manifest being read is a file-like object.
michael@0 477 test['manifest'] = filename
michael@0 478
michael@0 479 # determine the path
michael@0 480 path = test.get('path', section)
michael@0 481 _relpath = path
michael@0 482 if '://' not in path: # don't futz with URLs
michael@0 483 path = normalize_path(path)
michael@0 484 if here and not os.path.isabs(path):
michael@0 485 path = os.path.normpath(os.path.join(here, path))
michael@0 486
michael@0 487 # Microoptimization, because relpath is quite expensive.
michael@0 488 # We know that rootdir is an absolute path or empty. If path
michael@0 489 # starts with rootdir, then path is also absolute and the tail
michael@0 490 # of the path is the relative path (possibly non-normalized,
michael@0 491 # when here is unknown).
michael@0 492 # For this to work rootdir needs to be terminated with a path
michael@0 493 # separator, so that references to sibling directories with
michael@0 494 # a common prefix don't get misscomputed (e.g. /root and
michael@0 495 # /rootbeer/file).
michael@0 496 # When the rootdir is unknown, the relpath needs to be left
michael@0 497 # unchanged. We use an empty string as rootdir in that case,
michael@0 498 # which leaves relpath unchanged after slicing.
michael@0 499 if path.startswith(rootdir):
michael@0 500 _relpath = path[len(rootdir):]
michael@0 501 else:
michael@0 502 _relpath = relpath(path, rootdir)
michael@0 503
michael@0 504 test['subsuite'] = subsuite
michael@0 505 test['path'] = path
michael@0 506 test['relpath'] = _relpath
michael@0 507
michael@0 508 # append the item
michael@0 509 self.tests.append(test)
michael@0 510
michael@0 511 def read(self, *filenames, **defaults):
michael@0 512 """
michael@0 513 read and add manifests from file paths or file-like objects
michael@0 514
michael@0 515 filenames -- file paths or file-like objects to read as manifests
michael@0 516 defaults -- default variables
michael@0 517 """
michael@0 518
michael@0 519 # ensure all files exist
michael@0 520 missing = [filename for filename in filenames
michael@0 521 if isinstance(filename, string) and not os.path.exists(filename) ]
michael@0 522 if missing:
michael@0 523 raise IOError('Missing files: %s' % ', '.join(missing))
michael@0 524
michael@0 525 # default variables
michael@0 526 _defaults = defaults.copy() or self._defaults.copy()
michael@0 527 _defaults.setdefault('here', None)
michael@0 528
michael@0 529 # process each file
michael@0 530 for filename in filenames:
michael@0 531 # set the per file defaults
michael@0 532 defaults = _defaults.copy()
michael@0 533 here = None
michael@0 534 if isinstance(filename, string):
michael@0 535 here = os.path.dirname(os.path.abspath(filename))
michael@0 536 defaults['here'] = here # directory of master .ini file
michael@0 537
michael@0 538 if self.rootdir is None:
michael@0 539 # set the root directory
michael@0 540 # == the directory of the first manifest given
michael@0 541 self.rootdir = here
michael@0 542
michael@0 543 self._read(here, filename, defaults)
michael@0 544
michael@0 545
michael@0 546 ### methods for querying manifests
michael@0 547
michael@0 548 def query(self, *checks, **kw):
michael@0 549 """
michael@0 550 general query function for tests
michael@0 551 - checks : callable conditions to test if the test fulfills the query
michael@0 552 """
michael@0 553 tests = kw.get('tests', None)
michael@0 554 if tests is None:
michael@0 555 tests = self.tests
michael@0 556 retval = []
michael@0 557 for test in tests:
michael@0 558 for check in checks:
michael@0 559 if not check(test):
michael@0 560 break
michael@0 561 else:
michael@0 562 retval.append(test)
michael@0 563 return retval
michael@0 564
michael@0 565 def get(self, _key=None, inverse=False, tags=None, tests=None, **kwargs):
michael@0 566 # TODO: pass a dict instead of kwargs since you might hav
michael@0 567 # e.g. 'inverse' as a key in the dict
michael@0 568
michael@0 569 # TODO: tags should just be part of kwargs with None values
michael@0 570 # (None == any is kinda weird, but probably still better)
michael@0 571
michael@0 572 # fix up tags
michael@0 573 if tags:
michael@0 574 tags = set(tags)
michael@0 575 else:
michael@0 576 tags = set()
michael@0 577
michael@0 578 # make some check functions
michael@0 579 if inverse:
michael@0 580 has_tags = lambda test: not tags.intersection(test.keys())
michael@0 581 def dict_query(test):
michael@0 582 for key, value in kwargs.items():
michael@0 583 if test.get(key) == value:
michael@0 584 return False
michael@0 585 return True
michael@0 586 else:
michael@0 587 has_tags = lambda test: tags.issubset(test.keys())
michael@0 588 def dict_query(test):
michael@0 589 for key, value in kwargs.items():
michael@0 590 if test.get(key) != value:
michael@0 591 return False
michael@0 592 return True
michael@0 593
michael@0 594 # query the tests
michael@0 595 tests = self.query(has_tags, dict_query, tests=tests)
michael@0 596
michael@0 597 # if a key is given, return only a list of that key
michael@0 598 # useful for keys like 'name' or 'path'
michael@0 599 if _key:
michael@0 600 return [test[_key] for test in tests]
michael@0 601
michael@0 602 # return the tests
michael@0 603 return tests
michael@0 604
michael@0 605 def manifests(self, tests=None):
michael@0 606 """
michael@0 607 return manifests in order in which they appear in the tests
michael@0 608 """
michael@0 609 if tests is None:
michael@0 610 # Make sure to return all the manifests, even ones without tests.
michael@0 611 return self.manifest_defaults.keys()
michael@0 612
michael@0 613 manifests = []
michael@0 614 for test in tests:
michael@0 615 manifest = test.get('manifest')
michael@0 616 if not manifest:
michael@0 617 continue
michael@0 618 if manifest not in manifests:
michael@0 619 manifests.append(manifest)
michael@0 620 return manifests
michael@0 621
michael@0 622 def paths(self):
michael@0 623 return [i['path'] for i in self.tests]
michael@0 624
michael@0 625
michael@0 626 ### methods for auditing
michael@0 627
michael@0 628 def missing(self, tests=None):
michael@0 629 """return list of tests that do not exist on the filesystem"""
michael@0 630 if tests is None:
michael@0 631 tests = self.tests
michael@0 632 return [test for test in tests
michael@0 633 if not os.path.exists(test['path'])]
michael@0 634
michael@0 635 def verifyDirectory(self, directories, pattern=None, extensions=None):
michael@0 636 """
michael@0 637 checks what is on the filesystem vs what is in a manifest
michael@0 638 returns a 2-tuple of sets:
michael@0 639 (missing_from_filesystem, missing_from_manifest)
michael@0 640 """
michael@0 641
michael@0 642 files = set([])
michael@0 643 if isinstance(directories, basestring):
michael@0 644 directories = [directories]
michael@0 645
michael@0 646 # get files in directories
michael@0 647 for directory in directories:
michael@0 648 for dirpath, dirnames, filenames in os.walk(directory, topdown=True):
michael@0 649
michael@0 650 # only add files that match a pattern
michael@0 651 if pattern:
michael@0 652 filenames = fnmatch.filter(filenames, pattern)
michael@0 653
michael@0 654 # only add files that have one of the extensions
michael@0 655 if extensions:
michael@0 656 filenames = [filename for filename in filenames
michael@0 657 if os.path.splitext(filename)[-1] in extensions]
michael@0 658
michael@0 659 files.update([os.path.join(dirpath, filename) for filename in filenames])
michael@0 660
michael@0 661 paths = set(self.paths())
michael@0 662 missing_from_filesystem = paths.difference(files)
michael@0 663 missing_from_manifest = files.difference(paths)
michael@0 664 return (missing_from_filesystem, missing_from_manifest)
michael@0 665
michael@0 666
michael@0 667 ### methods for output
michael@0 668
michael@0 669 def write(self, fp=sys.stdout, rootdir=None,
michael@0 670 global_tags=None, global_kwargs=None,
michael@0 671 local_tags=None, local_kwargs=None):
michael@0 672 """
michael@0 673 write a manifest given a query
michael@0 674 global and local options will be munged to do the query
michael@0 675 globals will be written to the top of the file
michael@0 676 locals (if given) will be written per test
michael@0 677 """
michael@0 678
michael@0 679 # open file if `fp` given as string
michael@0 680 close = False
michael@0 681 if isinstance(fp, string):
michael@0 682 fp = file(fp, 'w')
michael@0 683 close = True
michael@0 684
michael@0 685 # root directory
michael@0 686 if rootdir is None:
michael@0 687 rootdir = self.rootdir
michael@0 688
michael@0 689 # sanitize input
michael@0 690 global_tags = global_tags or set()
michael@0 691 local_tags = local_tags or set()
michael@0 692 global_kwargs = global_kwargs or {}
michael@0 693 local_kwargs = local_kwargs or {}
michael@0 694
michael@0 695 # create the query
michael@0 696 tags = set([])
michael@0 697 tags.update(global_tags)
michael@0 698 tags.update(local_tags)
michael@0 699 kwargs = {}
michael@0 700 kwargs.update(global_kwargs)
michael@0 701 kwargs.update(local_kwargs)
michael@0 702
michael@0 703 # get matching tests
michael@0 704 tests = self.get(tags=tags, **kwargs)
michael@0 705
michael@0 706 # print the .ini manifest
michael@0 707 if global_tags or global_kwargs:
michael@0 708 print >> fp, '[DEFAULT]'
michael@0 709 for tag in global_tags:
michael@0 710 print >> fp, '%s =' % tag
michael@0 711 for key, value in global_kwargs.items():
michael@0 712 print >> fp, '%s = %s' % (key, value)
michael@0 713 print >> fp
michael@0 714
michael@0 715 for test in tests:
michael@0 716 test = test.copy() # don't overwrite
michael@0 717
michael@0 718 path = test['name']
michael@0 719 if not os.path.isabs(path):
michael@0 720 path = test['path']
michael@0 721 if self.rootdir:
michael@0 722 path = relpath(test['path'], self.rootdir)
michael@0 723 path = denormalize_path(path)
michael@0 724 print >> fp, '[%s]' % path
michael@0 725
michael@0 726 # reserved keywords:
michael@0 727 reserved = ['path', 'name', 'here', 'manifest', 'relpath']
michael@0 728 for key in sorted(test.keys()):
michael@0 729 if key in reserved:
michael@0 730 continue
michael@0 731 if key in global_kwargs:
michael@0 732 continue
michael@0 733 if key in global_tags and not test[key]:
michael@0 734 continue
michael@0 735 print >> fp, '%s = %s' % (key, test[key])
michael@0 736 print >> fp
michael@0 737
michael@0 738 if close:
michael@0 739 # close the created file
michael@0 740 fp.close()
michael@0 741
michael@0 742 def __str__(self):
michael@0 743 fp = StringIO()
michael@0 744 self.write(fp=fp)
michael@0 745 value = fp.getvalue()
michael@0 746 return value
michael@0 747
michael@0 748 def copy(self, directory, rootdir=None, *tags, **kwargs):
michael@0 749 """
michael@0 750 copy the manifests and associated tests
michael@0 751 - directory : directory to copy to
michael@0 752 - rootdir : root directory to copy to (if not given from manifests)
michael@0 753 - tags : keywords the tests must have
michael@0 754 - kwargs : key, values the tests must match
michael@0 755 """
michael@0 756 # XXX note that copy does *not* filter the tests out of the
michael@0 757 # resulting manifest; it just stupidly copies them over.
michael@0 758 # ideally, it would reread the manifests and filter out the
michael@0 759 # tests that don't match *tags and **kwargs
michael@0 760
michael@0 761 # destination
michael@0 762 if not os.path.exists(directory):
michael@0 763 os.path.makedirs(directory)
michael@0 764 else:
michael@0 765 # sanity check
michael@0 766 assert os.path.isdir(directory)
michael@0 767
michael@0 768 # tests to copy
michael@0 769 tests = self.get(tags=tags, **kwargs)
michael@0 770 if not tests:
michael@0 771 return # nothing to do!
michael@0 772
michael@0 773 # root directory
michael@0 774 if rootdir is None:
michael@0 775 rootdir = self.rootdir
michael@0 776
michael@0 777 # copy the manifests + tests
michael@0 778 manifests = [relpath(manifest, rootdir) for manifest in self.manifests()]
michael@0 779 for manifest in manifests:
michael@0 780 destination = os.path.join(directory, manifest)
michael@0 781 dirname = os.path.dirname(destination)
michael@0 782 if not os.path.exists(dirname):
michael@0 783 os.makedirs(dirname)
michael@0 784 else:
michael@0 785 # sanity check
michael@0 786 assert os.path.isdir(dirname)
michael@0 787 shutil.copy(os.path.join(rootdir, manifest), destination)
michael@0 788 for test in tests:
michael@0 789 if os.path.isabs(test['name']):
michael@0 790 continue
michael@0 791 source = test['path']
michael@0 792 if not os.path.exists(source):
michael@0 793 print >> sys.stderr, "Missing test: '%s' does not exist!" % source
michael@0 794 continue
michael@0 795 # TODO: should err on strict
michael@0 796 destination = os.path.join(directory, relpath(test['path'], rootdir))
michael@0 797 shutil.copy(source, destination)
michael@0 798 # TODO: ensure that all of the tests are below the from_dir
michael@0 799
michael@0 800 def update(self, from_dir, rootdir=None, *tags, **kwargs):
michael@0 801 """
michael@0 802 update the tests as listed in a manifest from a directory
michael@0 803 - from_dir : directory where the tests live
michael@0 804 - rootdir : root directory to copy to (if not given from manifests)
michael@0 805 - tags : keys the tests must have
michael@0 806 - kwargs : key, values the tests must match
michael@0 807 """
michael@0 808
michael@0 809 # get the tests
michael@0 810 tests = self.get(tags=tags, **kwargs)
michael@0 811
michael@0 812 # get the root directory
michael@0 813 if not rootdir:
michael@0 814 rootdir = self.rootdir
michael@0 815
michael@0 816 # copy them!
michael@0 817 for test in tests:
michael@0 818 if not os.path.isabs(test['name']):
michael@0 819 _relpath = relpath(test['path'], rootdir)
michael@0 820 source = os.path.join(from_dir, _relpath)
michael@0 821 if not os.path.exists(source):
michael@0 822 # TODO err on strict
michael@0 823 print >> sys.stderr, "Missing test: '%s'; skipping" % test['name']
michael@0 824 continue
michael@0 825 destination = os.path.join(rootdir, _relpath)
michael@0 826 shutil.copy(source, destination)
michael@0 827
michael@0 828 ### directory importers
michael@0 829
michael@0 830 @classmethod
michael@0 831 def _walk_directories(cls, directories, function, pattern=None, ignore=()):
michael@0 832 """
michael@0 833 internal function to import directories
michael@0 834 """
michael@0 835
michael@0 836 class FilteredDirectoryContents(object):
michael@0 837 """class to filter directory contents"""
michael@0 838
michael@0 839 sort = sorted
michael@0 840
michael@0 841 def __init__(self, pattern=pattern, ignore=ignore, cache=None):
michael@0 842 if pattern is None:
michael@0 843 pattern = set()
michael@0 844 if isinstance(pattern, basestring):
michael@0 845 pattern = [pattern]
michael@0 846 self.patterns = pattern
michael@0 847 self.ignore = set(ignore)
michael@0 848
michael@0 849 # cache of (dirnames, filenames) keyed on directory real path
michael@0 850 # assumes volume is frozen throughout scope
michael@0 851 self._cache = cache or {}
michael@0 852
michael@0 853 def __call__(self, directory):
michael@0 854 """returns 2-tuple: dirnames, filenames"""
michael@0 855 directory = os.path.realpath(directory)
michael@0 856 if directory not in self._cache:
michael@0 857 dirnames, filenames = self.contents(directory)
michael@0 858
michael@0 859 # filter out directories without progeny
michael@0 860 # XXX recursive: should keep track of seen directories
michael@0 861 dirnames = [ dirname for dirname in dirnames
michael@0 862 if not self.empty(os.path.join(directory, dirname)) ]
michael@0 863
michael@0 864 self._cache[directory] = (tuple(dirnames), filenames)
michael@0 865
michael@0 866 # return cached values
michael@0 867 return self._cache[directory]
michael@0 868
michael@0 869 def empty(self, directory):
michael@0 870 """
michael@0 871 returns if a directory and its descendents are empty
michael@0 872 """
michael@0 873 return self(directory) == ((), ())
michael@0 874
michael@0 875 def contents(self, directory, sort=None):
michael@0 876 """
michael@0 877 return directory contents as (dirnames, filenames)
michael@0 878 with `ignore` and `pattern` applied
michael@0 879 """
michael@0 880
michael@0 881 if sort is None:
michael@0 882 sort = self.sort
michael@0 883
michael@0 884 # split directories and files
michael@0 885 dirnames = []
michael@0 886 filenames = []
michael@0 887 for item in os.listdir(directory):
michael@0 888 path = os.path.join(directory, item)
michael@0 889 if os.path.isdir(path):
michael@0 890 dirnames.append(item)
michael@0 891 else:
michael@0 892 # XXX not sure what to do if neither a file or directory
michael@0 893 # (if anything)
michael@0 894 assert os.path.isfile(path)
michael@0 895 filenames.append(item)
michael@0 896
michael@0 897 # filter contents;
michael@0 898 # this could be done in situ re the above for loop
michael@0 899 # but it is really disparate in intent
michael@0 900 # and could conceivably go to a separate method
michael@0 901 dirnames = [dirname for dirname in dirnames
michael@0 902 if dirname not in self.ignore]
michael@0 903 filenames = set(filenames)
michael@0 904 # we use set functionality to filter filenames
michael@0 905 if self.patterns:
michael@0 906 matches = set()
michael@0 907 matches.update(*[fnmatch.filter(filenames, pattern)
michael@0 908 for pattern in self.patterns])
michael@0 909 filenames = matches
michael@0 910
michael@0 911 if sort is not None:
michael@0 912 # sort dirnames, filenames
michael@0 913 dirnames = sort(dirnames)
michael@0 914 filenames = sort(filenames)
michael@0 915
michael@0 916 return (tuple(dirnames), tuple(filenames))
michael@0 917
michael@0 918 # make a filtered directory object
michael@0 919 directory_contents = FilteredDirectoryContents(pattern=pattern, ignore=ignore)
michael@0 920
michael@0 921 # walk the directories, generating manifests
michael@0 922 for index, directory in enumerate(directories):
michael@0 923
michael@0 924 for dirpath, dirnames, filenames in os.walk(directory):
michael@0 925
michael@0 926 # get the directory contents from the caching object
michael@0 927 _dirnames, filenames = directory_contents(dirpath)
michael@0 928 # filter out directory names
michael@0 929 dirnames[:] = _dirnames
michael@0 930
michael@0 931 # call callback function
michael@0 932 function(directory, dirpath, dirnames, filenames)
michael@0 933
michael@0 934 @classmethod
michael@0 935 def populate_directory_manifests(cls, directories, filename, pattern=None, ignore=(), overwrite=False):
michael@0 936 """
michael@0 937 walks directories and writes manifests of name `filename` in-place; returns `cls` instance populated
michael@0 938 with the given manifests
michael@0 939
michael@0 940 filename -- filename of manifests to write
michael@0 941 pattern -- shell pattern (glob) or patterns of filenames to match
michael@0 942 ignore -- directory names to ignore
michael@0 943 overwrite -- whether to overwrite existing files of given name
michael@0 944 """
michael@0 945
michael@0 946 manifest_dict = {}
michael@0 947 seen = [] # top-level directories seen
michael@0 948
michael@0 949 if os.path.basename(filename) != filename:
michael@0 950 raise IOError("filename should not include directory name")
michael@0 951
michael@0 952 # no need to hit directories more than once
michael@0 953 _directories = directories
michael@0 954 directories = []
michael@0 955 for directory in _directories:
michael@0 956 if directory not in directories:
michael@0 957 directories.append(directory)
michael@0 958
michael@0 959 def callback(directory, dirpath, dirnames, filenames):
michael@0 960 """write a manifest for each directory"""
michael@0 961
michael@0 962 manifest_path = os.path.join(dirpath, filename)
michael@0 963 if (dirnames or filenames) and not (os.path.exists(manifest_path) and overwrite):
michael@0 964 with file(manifest_path, 'w') as manifest:
michael@0 965 for dirname in dirnames:
michael@0 966 print >> manifest, '[include:%s]' % os.path.join(dirname, filename)
michael@0 967 for _filename in filenames:
michael@0 968 print >> manifest, '[%s]' % _filename
michael@0 969
michael@0 970 # add to list of manifests
michael@0 971 manifest_dict.setdefault(directory, manifest_path)
michael@0 972
michael@0 973 # walk the directories to gather files
michael@0 974 cls._walk_directories(directories, callback, pattern=pattern, ignore=ignore)
michael@0 975 # get manifests
michael@0 976 manifests = [manifest_dict[directory] for directory in _directories]
michael@0 977
michael@0 978 # create a `cls` instance with the manifests
michael@0 979 return cls(manifests=manifests)
michael@0 980
michael@0 981 @classmethod
michael@0 982 def from_directories(cls, directories, pattern=None, ignore=(), write=None, relative_to=None):
michael@0 983 """
michael@0 984 convert directories to a simple manifest; returns ManifestParser instance
michael@0 985
michael@0 986 pattern -- shell pattern (glob) or patterns of filenames to match
michael@0 987 ignore -- directory names to ignore
michael@0 988 write -- filename or file-like object of manifests to write;
michael@0 989 if `None` then a StringIO instance will be created
michael@0 990 relative_to -- write paths relative to this path;
michael@0 991 if false then the paths are absolute
michael@0 992 """
michael@0 993
michael@0 994
michael@0 995 # determine output
michael@0 996 opened_manifest_file = None # name of opened manifest file
michael@0 997 absolute = not relative_to # whether to output absolute path names as names
michael@0 998 if isinstance(write, string):
michael@0 999 opened_manifest_file = write
michael@0 1000 write = file(write, 'w')
michael@0 1001 if write is None:
michael@0 1002 write = StringIO()
michael@0 1003
michael@0 1004 # walk the directories, generating manifests
michael@0 1005 def callback(directory, dirpath, dirnames, filenames):
michael@0 1006
michael@0 1007 # absolute paths
michael@0 1008 filenames = [os.path.join(dirpath, filename)
michael@0 1009 for filename in filenames]
michael@0 1010 # ensure new manifest isn't added
michael@0 1011 filenames = [filename for filename in filenames
michael@0 1012 if filename != opened_manifest_file]
michael@0 1013 # normalize paths
michael@0 1014 if not absolute and relative_to:
michael@0 1015 filenames = [relpath(filename, relative_to)
michael@0 1016 for filename in filenames]
michael@0 1017
michael@0 1018 # write to manifest
michael@0 1019 print >> write, '\n'.join(['[%s]' % denormalize_path(filename)
michael@0 1020 for filename in filenames])
michael@0 1021
michael@0 1022
michael@0 1023 cls._walk_directories(directories, callback, pattern=pattern, ignore=ignore)
michael@0 1024
michael@0 1025 if opened_manifest_file:
michael@0 1026 # close file
michael@0 1027 write.close()
michael@0 1028 manifests = [opened_manifest_file]
michael@0 1029 else:
michael@0 1030 # manifests/write is a file-like object;
michael@0 1031 # rewind buffer
michael@0 1032 write.flush()
michael@0 1033 write.seek(0)
michael@0 1034 manifests = [write]
michael@0 1035
michael@0 1036
michael@0 1037 # make a ManifestParser instance
michael@0 1038 return cls(manifests=manifests)
michael@0 1039
michael@0 1040 convert = ManifestParser.from_directories
michael@0 1041
michael@0 1042
michael@0 1043 class TestManifest(ManifestParser):
michael@0 1044 """
michael@0 1045 apply logic to manifests; this is your integration layer :)
michael@0 1046 specific harnesses may subclass from this if they need more logic
michael@0 1047 """
michael@0 1048
michael@0 1049 def filter(self, values, tests):
michael@0 1050 """
michael@0 1051 filter on a specific list tag, e.g.:
michael@0 1052 run-if = os == win linux
michael@0 1053 skip-if = os == mac
michael@0 1054 """
michael@0 1055
michael@0 1056 # tags:
michael@0 1057 run_tag = 'run-if'
michael@0 1058 skip_tag = 'skip-if'
michael@0 1059 fail_tag = 'fail-if'
michael@0 1060
michael@0 1061 # loop over test
michael@0 1062 for test in tests:
michael@0 1063 reason = None # reason to disable
michael@0 1064
michael@0 1065 # tagged-values to run
michael@0 1066 if run_tag in test:
michael@0 1067 condition = test[run_tag]
michael@0 1068 if not parse(condition, **values):
michael@0 1069 reason = '%s: %s' % (run_tag, condition)
michael@0 1070
michael@0 1071 # tagged-values to skip
michael@0 1072 if skip_tag in test:
michael@0 1073 condition = test[skip_tag]
michael@0 1074 if parse(condition, **values):
michael@0 1075 reason = '%s: %s' % (skip_tag, condition)
michael@0 1076
michael@0 1077 # mark test as disabled if there's a reason
michael@0 1078 if reason:
michael@0 1079 test.setdefault('disabled', reason)
michael@0 1080
michael@0 1081 # mark test as a fail if so indicated
michael@0 1082 if fail_tag in test:
michael@0 1083 condition = test[fail_tag]
michael@0 1084 if parse(condition, **values):
michael@0 1085 test['expected'] = 'fail'
michael@0 1086
michael@0 1087 def active_tests(self, exists=True, disabled=True, options=None, **values):
michael@0 1088 """
michael@0 1089 - exists : return only existing tests
michael@0 1090 - disabled : whether to return disabled tests
michael@0 1091 - tags : keys and values to filter on (e.g. `os = linux mac`)
michael@0 1092 """
michael@0 1093 tests = [i.copy() for i in self.tests] # shallow copy
michael@0 1094
michael@0 1095 # Filter on current subsuite
michael@0 1096 if options:
michael@0 1097 if options.subsuite:
michael@0 1098 tests = [test for test in tests if options.subsuite == test['subsuite']]
michael@0 1099 else:
michael@0 1100 tests = [test for test in tests if not test['subsuite']]
michael@0 1101
michael@0 1102 # mark all tests as passing unless indicated otherwise
michael@0 1103 for test in tests:
michael@0 1104 test['expected'] = test.get('expected', 'pass')
michael@0 1105
michael@0 1106 # ignore tests that do not exist
michael@0 1107 if exists:
michael@0 1108 tests = [test for test in tests if os.path.exists(test['path'])]
michael@0 1109
michael@0 1110 # filter by tags
michael@0 1111 self.filter(values, tests)
michael@0 1112
michael@0 1113 # ignore disabled tests if specified
michael@0 1114 if not disabled:
michael@0 1115 tests = [test for test in tests
michael@0 1116 if not 'disabled' in test]
michael@0 1117
michael@0 1118 # return active tests
michael@0 1119 return tests
michael@0 1120
michael@0 1121 def test_paths(self):
michael@0 1122 return [test['path'] for test in self.active_tests()]
michael@0 1123
michael@0 1124
michael@0 1125 ### command line attributes
michael@0 1126
michael@0 1127 class ParserError(Exception):
michael@0 1128 """error for exceptions while parsing the command line"""
michael@0 1129
michael@0 1130 def parse_args(_args):
michael@0 1131 """
michael@0 1132 parse and return:
michael@0 1133 --keys=value (or --key value)
michael@0 1134 -tags
michael@0 1135 args
michael@0 1136 """
michael@0 1137
michael@0 1138 # return values
michael@0 1139 _dict = {}
michael@0 1140 tags = []
michael@0 1141 args = []
michael@0 1142
michael@0 1143 # parse the arguments
michael@0 1144 key = None
michael@0 1145 for arg in _args:
michael@0 1146 if arg.startswith('---'):
michael@0 1147 raise ParserError("arguments should start with '-' or '--' only")
michael@0 1148 elif arg.startswith('--'):
michael@0 1149 if key:
michael@0 1150 raise ParserError("Key %s still open" % key)
michael@0 1151 key = arg[2:]
michael@0 1152 if '=' in key:
michael@0 1153 key, value = key.split('=', 1)
michael@0 1154 _dict[key] = value
michael@0 1155 key = None
michael@0 1156 continue
michael@0 1157 elif arg.startswith('-'):
michael@0 1158 if key:
michael@0 1159 raise ParserError("Key %s still open" % key)
michael@0 1160 tags.append(arg[1:])
michael@0 1161 continue
michael@0 1162 else:
michael@0 1163 if key:
michael@0 1164 _dict[key] = arg
michael@0 1165 continue
michael@0 1166 args.append(arg)
michael@0 1167
michael@0 1168 # return values
michael@0 1169 return (_dict, tags, args)
michael@0 1170
michael@0 1171
michael@0 1172 ### classes for subcommands
michael@0 1173
michael@0 1174 class CLICommand(object):
michael@0 1175 usage = '%prog [options] command'
michael@0 1176 def __init__(self, parser):
michael@0 1177 self._parser = parser # master parser
michael@0 1178 def parser(self):
michael@0 1179 return OptionParser(usage=self.usage, description=self.__doc__,
michael@0 1180 add_help_option=False)
michael@0 1181
michael@0 1182 class Copy(CLICommand):
michael@0 1183 usage = '%prog [options] copy manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...'
michael@0 1184 def __call__(self, options, args):
michael@0 1185 # parse the arguments
michael@0 1186 try:
michael@0 1187 kwargs, tags, args = parse_args(args)
michael@0 1188 except ParserError, e:
michael@0 1189 self._parser.error(e.message)
michael@0 1190
michael@0 1191 # make sure we have some manifests, otherwise it will
michael@0 1192 # be quite boring
michael@0 1193 if not len(args) == 2:
michael@0 1194 HelpCLI(self._parser)(options, ['copy'])
michael@0 1195 return
michael@0 1196
michael@0 1197 # read the manifests
michael@0 1198 # TODO: should probably ensure these exist here
michael@0 1199 manifests = ManifestParser()
michael@0 1200 manifests.read(args[0])
michael@0 1201
michael@0 1202 # print the resultant query
michael@0 1203 manifests.copy(args[1], None, *tags, **kwargs)
michael@0 1204
michael@0 1205
michael@0 1206 class CreateCLI(CLICommand):
michael@0 1207 """
michael@0 1208 create a manifest from a list of directories
michael@0 1209 """
michael@0 1210 usage = '%prog [options] create directory <directory> <...>'
michael@0 1211
michael@0 1212 def parser(self):
michael@0 1213 parser = CLICommand.parser(self)
michael@0 1214 parser.add_option('-p', '--pattern', dest='pattern',
michael@0 1215 help="glob pattern for files")
michael@0 1216 parser.add_option('-i', '--ignore', dest='ignore',
michael@0 1217 default=[], action='append',
michael@0 1218 help='directories to ignore')
michael@0 1219 parser.add_option('-w', '--in-place', dest='in_place',
michael@0 1220 help='Write .ini files in place; filename to write to')
michael@0 1221 return parser
michael@0 1222
michael@0 1223 def __call__(self, _options, args):
michael@0 1224 parser = self.parser()
michael@0 1225 options, args = parser.parse_args(args)
michael@0 1226
michael@0 1227 # need some directories
michael@0 1228 if not len(args):
michael@0 1229 parser.print_usage()
michael@0 1230 return
michael@0 1231
michael@0 1232 # add the directories to the manifest
michael@0 1233 for arg in args:
michael@0 1234 assert os.path.exists(arg)
michael@0 1235 assert os.path.isdir(arg)
michael@0 1236 manifest = convert(args, pattern=options.pattern, ignore=options.ignore,
michael@0 1237 write=options.in_place)
michael@0 1238 if manifest:
michael@0 1239 print manifest
michael@0 1240
michael@0 1241
michael@0 1242 class WriteCLI(CLICommand):
michael@0 1243 """
michael@0 1244 write a manifest based on a query
michael@0 1245 """
michael@0 1246 usage = '%prog [options] write manifest <manifest> -tag1 -tag2 --key1=value1 --key2=value2 ...'
michael@0 1247 def __call__(self, options, args):
michael@0 1248
michael@0 1249 # parse the arguments
michael@0 1250 try:
michael@0 1251 kwargs, tags, args = parse_args(args)
michael@0 1252 except ParserError, e:
michael@0 1253 self._parser.error(e.message)
michael@0 1254
michael@0 1255 # make sure we have some manifests, otherwise it will
michael@0 1256 # be quite boring
michael@0 1257 if not args:
michael@0 1258 HelpCLI(self._parser)(options, ['write'])
michael@0 1259 return
michael@0 1260
michael@0 1261 # read the manifests
michael@0 1262 # TODO: should probably ensure these exist here
michael@0 1263 manifests = ManifestParser()
michael@0 1264 manifests.read(*args)
michael@0 1265
michael@0 1266 # print the resultant query
michael@0 1267 manifests.write(global_tags=tags, global_kwargs=kwargs)
michael@0 1268
michael@0 1269
michael@0 1270 class HelpCLI(CLICommand):
michael@0 1271 """
michael@0 1272 get help on a command
michael@0 1273 """
michael@0 1274 usage = '%prog [options] help [command]'
michael@0 1275
michael@0 1276 def __call__(self, options, args):
michael@0 1277 if len(args) == 1 and args[0] in commands:
michael@0 1278 commands[args[0]](self._parser).parser().print_help()
michael@0 1279 else:
michael@0 1280 self._parser.print_help()
michael@0 1281 print '\nCommands:'
michael@0 1282 for command in sorted(commands):
michael@0 1283 print ' %s : %s' % (command, commands[command].__doc__.strip())
michael@0 1284
michael@0 1285 class UpdateCLI(CLICommand):
michael@0 1286 """
michael@0 1287 update the tests as listed in a manifest from a directory
michael@0 1288 """
michael@0 1289 usage = '%prog [options] update manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...'
michael@0 1290
michael@0 1291 def __call__(self, options, args):
michael@0 1292 # parse the arguments
michael@0 1293 try:
michael@0 1294 kwargs, tags, args = parse_args(args)
michael@0 1295 except ParserError, e:
michael@0 1296 self._parser.error(e.message)
michael@0 1297
michael@0 1298 # make sure we have some manifests, otherwise it will
michael@0 1299 # be quite boring
michael@0 1300 if not len(args) == 2:
michael@0 1301 HelpCLI(self._parser)(options, ['update'])
michael@0 1302 return
michael@0 1303
michael@0 1304 # read the manifests
michael@0 1305 # TODO: should probably ensure these exist here
michael@0 1306 manifests = ManifestParser()
michael@0 1307 manifests.read(args[0])
michael@0 1308
michael@0 1309 # print the resultant query
michael@0 1310 manifests.update(args[1], None, *tags, **kwargs)
michael@0 1311
michael@0 1312
michael@0 1313 # command -> class mapping
michael@0 1314 commands = { 'create': CreateCLI,
michael@0 1315 'help': HelpCLI,
michael@0 1316 'update': UpdateCLI,
michael@0 1317 'write': WriteCLI }
michael@0 1318
michael@0 1319 def main(args=sys.argv[1:]):
michael@0 1320 """console_script entry point"""
michael@0 1321
michael@0 1322 # set up an option parser
michael@0 1323 usage = '%prog [options] [command] ...'
michael@0 1324 description = "%s. Use `help` to display commands" % __doc__.strip()
michael@0 1325 parser = OptionParser(usage=usage, description=description)
michael@0 1326 parser.add_option('-s', '--strict', dest='strict',
michael@0 1327 action='store_true', default=False,
michael@0 1328 help='adhere strictly to errors')
michael@0 1329 parser.disable_interspersed_args()
michael@0 1330
michael@0 1331 options, args = parser.parse_args(args)
michael@0 1332
michael@0 1333 if not args:
michael@0 1334 HelpCLI(parser)(options, args)
michael@0 1335 parser.exit()
michael@0 1336
michael@0 1337 # get the command
michael@0 1338 command = args[0]
michael@0 1339 if command not in commands:
michael@0 1340 parser.error("Command must be one of %s (you gave '%s')" % (', '.join(sorted(commands.keys())), command))
michael@0 1341
michael@0 1342 handler = commands[command](parser)
michael@0 1343 handler(options, args[1:])
michael@0 1344
michael@0 1345 if __name__ == '__main__':
michael@0 1346 main()

mercurial