testing/mozbase/manifestdestiny/manifestparser/manifestparser.py

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/testing/mozbase/manifestdestiny/manifestparser/manifestparser.py	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1346 @@
     1.4 +#!/usr/bin/env python
     1.5 +
     1.6 +# This Source Code Form is subject to the terms of the Mozilla Public
     1.7 +# License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.8 +# You can obtain one at http://mozilla.org/MPL/2.0/.
     1.9 +
    1.10 +"""
    1.11 +Mozilla universal manifest parser
    1.12 +"""
    1.13 +
    1.14 +__all__ = ['read_ini', # .ini reader
    1.15 +           'ManifestParser', 'TestManifest', 'convert', # manifest handling
    1.16 +           'parse', 'ParseError', 'ExpressionParser'] # conditional expression parser
    1.17 +
    1.18 +import fnmatch
    1.19 +import os
    1.20 +import re
    1.21 +import shutil
    1.22 +import sys
    1.23 +
    1.24 +from optparse import OptionParser
    1.25 +from StringIO import StringIO
    1.26 +
    1.27 +relpath = os.path.relpath
    1.28 +string = (basestring,)
    1.29 +
    1.30 +
    1.31 +# expr.py
    1.32 +# from:
    1.33 +# http://k0s.org/mozilla/hg/expressionparser
    1.34 +# http://hg.mozilla.org/users/tmielczarek_mozilla.com/expressionparser
    1.35 +
    1.36 +# Implements a top-down parser/evaluator for simple boolean expressions.
    1.37 +# ideas taken from http://effbot.org/zone/simple-top-down-parsing.htm
    1.38 +#
    1.39 +# Rough grammar:
    1.40 +# expr := literal
    1.41 +#       | '(' expr ')'
    1.42 +#       | expr '&&' expr
    1.43 +#       | expr '||' expr
    1.44 +#       | expr '==' expr
    1.45 +#       | expr '!=' expr
    1.46 +# literal := BOOL
    1.47 +#          | INT
    1.48 +#          | STRING
    1.49 +#          | IDENT
    1.50 +# BOOL   := true|false
    1.51 +# INT    := [0-9]+
    1.52 +# STRING := "[^"]*"
    1.53 +# IDENT  := [A-Za-z_]\w*
    1.54 +
    1.55 +# Identifiers take their values from a mapping dictionary passed as the second
    1.56 +# argument.
    1.57 +
    1.58 +# Glossary (see above URL for details):
    1.59 +# - nud: null denotation
    1.60 +# - led: left detonation
    1.61 +# - lbp: left binding power
    1.62 +# - rbp: right binding power
    1.63 +
    1.64 +class ident_token(object):
    1.65 +    def __init__(self, value):
    1.66 +        self.value = value
    1.67 +    def nud(self, parser):
    1.68 +        # identifiers take their value from the value mappings passed
    1.69 +        # to the parser
    1.70 +        return parser.value(self.value)
    1.71 +
    1.72 +class literal_token(object):
    1.73 +    def __init__(self, value):
    1.74 +        self.value = value
    1.75 +    def nud(self, parser):
    1.76 +        return self.value
    1.77 +
    1.78 +class eq_op_token(object):
    1.79 +    "=="
    1.80 +    def led(self, parser, left):
    1.81 +        return left == parser.expression(self.lbp)
    1.82 +
    1.83 +class neq_op_token(object):
    1.84 +    "!="
    1.85 +    def led(self, parser, left):
    1.86 +        return left != parser.expression(self.lbp)
    1.87 +
    1.88 +class not_op_token(object):
    1.89 +    "!"
    1.90 +    def nud(self, parser):
    1.91 +        return not parser.expression(100)
    1.92 +
    1.93 +class and_op_token(object):
    1.94 +    "&&"
    1.95 +    def led(self, parser, left):
    1.96 +        right = parser.expression(self.lbp)
    1.97 +        return left and right
    1.98 +
    1.99 +class or_op_token(object):
   1.100 +    "||"
   1.101 +    def led(self, parser, left):
   1.102 +        right = parser.expression(self.lbp)
   1.103 +        return left or right
   1.104 +
   1.105 +class lparen_token(object):
   1.106 +    "("
   1.107 +    def nud(self, parser):
   1.108 +        expr = parser.expression()
   1.109 +        parser.advance(rparen_token)
   1.110 +        return expr
   1.111 +
   1.112 +class rparen_token(object):
   1.113 +    ")"
   1.114 +
   1.115 +class end_token(object):
   1.116 +    """always ends parsing"""
   1.117 +
   1.118 +### derived literal tokens
   1.119 +
   1.120 +class bool_token(literal_token):
   1.121 +    def __init__(self, value):
   1.122 +        value = {'true':True, 'false':False}[value]
   1.123 +        literal_token.__init__(self, value)
   1.124 +
   1.125 +class int_token(literal_token):
   1.126 +    def __init__(self, value):
   1.127 +        literal_token.__init__(self, int(value))
   1.128 +
   1.129 +class string_token(literal_token):
   1.130 +    def __init__(self, value):
   1.131 +        literal_token.__init__(self, value[1:-1])
   1.132 +
   1.133 +precedence = [(end_token, rparen_token),
   1.134 +              (or_op_token,),
   1.135 +              (and_op_token,),
   1.136 +              (eq_op_token, neq_op_token),
   1.137 +              (lparen_token,),
   1.138 +              ]
   1.139 +for index, rank in enumerate(precedence):
   1.140 +    for token in rank:
   1.141 +        token.lbp = index # lbp = lowest left binding power
   1.142 +
   1.143 +class ParseError(Exception):
   1.144 +    """error parsing conditional expression"""
   1.145 +
   1.146 +class ExpressionParser(object):
   1.147 +    """
   1.148 +    A parser for a simple expression language.
   1.149 +
   1.150 +    The expression language can be described as follows::
   1.151 +
   1.152 +        EXPRESSION ::= LITERAL | '(' EXPRESSION ')' | '!' EXPRESSION | EXPRESSION OP EXPRESSION
   1.153 +        OP ::= '==' | '!=' | '&&' | '||'
   1.154 +        LITERAL ::= BOOL | INT | IDENT | STRING
   1.155 +        BOOL ::= 'true' | 'false'
   1.156 +        INT ::= [0-9]+
   1.157 +        IDENT ::= [a-zA-Z_]\w*
   1.158 +        STRING ::= '"' [^\"] '"' | ''' [^\'] '''
   1.159 +
   1.160 +    At its core, expressions consist of booleans, integers, identifiers and.
   1.161 +    strings. Booleans are one of *true* or *false*. Integers are a series
   1.162 +    of digits. Identifiers are a series of English letters and underscores.
   1.163 +    Strings are a pair of matching quote characters (single or double) with
   1.164 +    zero or more characters inside.
   1.165 +
   1.166 +    Expressions can be combined with operators: the equals (==) and not
   1.167 +    equals (!=) operators compare two expressions and produce a boolean. The
   1.168 +    and (&&) and or (||) operators take two expressions and produce the logical
   1.169 +    AND or OR value of them, respectively. An expression can also be prefixed
   1.170 +    with the not (!) operator, which produces its logical negation.
   1.171 +
   1.172 +    Finally, any expression may be contained within parentheses for grouping.
   1.173 +
   1.174 +    Identifiers take their values from the mapping provided.
   1.175 +    """
   1.176 +    def __init__(self, text, valuemapping, strict=False):
   1.177 +        """
   1.178 +        Initialize the parser
   1.179 +        :param text: The expression to parse as a string.
   1.180 +        :param valuemapping: A dict mapping identifier names to values.
   1.181 +        :param strict: If true, referencing an identifier that was not
   1.182 +                       provided in :valuemapping: will raise an error.
   1.183 +        """
   1.184 +        self.text = text
   1.185 +        self.valuemapping = valuemapping
   1.186 +        self.strict = strict
   1.187 +
   1.188 +    def _tokenize(self):
   1.189 +        """
   1.190 +        Lex the input text into tokens and yield them in sequence.
   1.191 +        """
   1.192 +        # scanner callbacks
   1.193 +        def bool_(scanner, t): return bool_token(t)
   1.194 +        def identifier(scanner, t): return ident_token(t)
   1.195 +        def integer(scanner, t): return int_token(t)
   1.196 +        def eq(scanner, t): return eq_op_token()
   1.197 +        def neq(scanner, t): return neq_op_token()
   1.198 +        def or_(scanner, t): return or_op_token()
   1.199 +        def and_(scanner, t): return and_op_token()
   1.200 +        def lparen(scanner, t): return lparen_token()
   1.201 +        def rparen(scanner, t): return rparen_token()
   1.202 +        def string_(scanner, t): return string_token(t)
   1.203 +        def not_(scanner, t): return not_op_token()
   1.204 +
   1.205 +        scanner = re.Scanner([
   1.206 +            # Note: keep these in sync with the class docstring above.
   1.207 +            (r"true|false", bool_),
   1.208 +            (r"[a-zA-Z_]\w*", identifier),
   1.209 +            (r"[0-9]+", integer),
   1.210 +            (r'("[^"]*")|(\'[^\']*\')', string_),
   1.211 +            (r"==", eq),
   1.212 +            (r"!=", neq),
   1.213 +            (r"\|\|", or_),
   1.214 +            (r"!", not_),
   1.215 +            (r"&&", and_),
   1.216 +            (r"\(", lparen),
   1.217 +            (r"\)", rparen),
   1.218 +            (r"\s+", None), # skip whitespace
   1.219 +            ])
   1.220 +        tokens, remainder = scanner.scan(self.text)
   1.221 +        for t in tokens:
   1.222 +            yield t
   1.223 +        yield end_token()
   1.224 +
   1.225 +    def value(self, ident):
   1.226 +        """
   1.227 +        Look up the value of |ident| in the value mapping passed in the
   1.228 +        constructor.
   1.229 +        """
   1.230 +        if self.strict:
   1.231 +            return self.valuemapping[ident]
   1.232 +        else:
   1.233 +            return self.valuemapping.get(ident, None)
   1.234 +
   1.235 +    def advance(self, expected):
   1.236 +        """
   1.237 +        Assert that the next token is an instance of |expected|, and advance
   1.238 +        to the next token.
   1.239 +        """
   1.240 +        if not isinstance(self.token, expected):
   1.241 +            raise Exception, "Unexpected token!"
   1.242 +        self.token = self.iter.next()
   1.243 +
   1.244 +    def expression(self, rbp=0):
   1.245 +        """
   1.246 +        Parse and return the value of an expression until a token with
   1.247 +        right binding power greater than rbp is encountered.
   1.248 +        """
   1.249 +        t = self.token
   1.250 +        self.token = self.iter.next()
   1.251 +        left = t.nud(self)
   1.252 +        while rbp < self.token.lbp:
   1.253 +            t = self.token
   1.254 +            self.token = self.iter.next()
   1.255 +            left = t.led(self, left)
   1.256 +        return left
   1.257 +
   1.258 +    def parse(self):
   1.259 +        """
   1.260 +        Parse and return the value of the expression in the text
   1.261 +        passed to the constructor. Raises a ParseError if the expression
   1.262 +        could not be parsed.
   1.263 +        """
   1.264 +        try:
   1.265 +            self.iter = self._tokenize()
   1.266 +            self.token = self.iter.next()
   1.267 +            return self.expression()
   1.268 +        except:
   1.269 +            raise ParseError("could not parse: %s; variables: %s" % (self.text, self.valuemapping))
   1.270 +
   1.271 +    __call__ = parse
   1.272 +
   1.273 +def parse(text, **values):
   1.274 +    """
   1.275 +    Parse and evaluate a boolean expression.
   1.276 +    :param text: The expression to parse, as a string.
   1.277 +    :param values: A dict containing a name to value mapping for identifiers
   1.278 +                   referenced in *text*.
   1.279 +    :rtype: the final value of the expression.
   1.280 +    :raises: :py:exc::ParseError: will be raised if parsing fails.
   1.281 +    """
   1.282 +    return ExpressionParser(text, values).parse()
   1.283 +
   1.284 +
   1.285 +### path normalization
   1.286 +
   1.287 +def normalize_path(path):
   1.288 +    """normalize a relative path"""
   1.289 +    if sys.platform.startswith('win'):
   1.290 +        return path.replace('/', os.path.sep)
   1.291 +    return path
   1.292 +
   1.293 +def denormalize_path(path):
   1.294 +    """denormalize a relative path"""
   1.295 +    if sys.platform.startswith('win'):
   1.296 +        return path.replace(os.path.sep, '/')
   1.297 +    return path
   1.298 +
   1.299 +
   1.300 +### .ini reader
   1.301 +
   1.302 +def read_ini(fp, variables=None, default='DEFAULT',
   1.303 +             comments=';#', separators=('=', ':'),
   1.304 +             strict=True):
   1.305 +    """
   1.306 +    read an .ini file and return a list of [(section, values)]
   1.307 +    - fp : file pointer or path to read
   1.308 +    - variables : default set of variables
   1.309 +    - default : name of the section for the default section
   1.310 +    - comments : characters that if they start a line denote a comment
   1.311 +    - separators : strings that denote key, value separation in order
   1.312 +    - strict : whether to be strict about parsing
   1.313 +    """
   1.314 +
   1.315 +    # variables
   1.316 +    variables = variables or {}
   1.317 +    sections = []
   1.318 +    key = value = None
   1.319 +    section_names = set()
   1.320 +    if isinstance(fp, basestring):
   1.321 +        fp = file(fp)
   1.322 +
   1.323 +    # read the lines
   1.324 +    for (linenum, line) in enumerate(fp.readlines(), start=1):
   1.325 +
   1.326 +        stripped = line.strip()
   1.327 +
   1.328 +        # ignore blank lines
   1.329 +        if not stripped:
   1.330 +            # reset key and value to avoid continuation lines
   1.331 +            key = value = None
   1.332 +            continue
   1.333 +
   1.334 +        # ignore comment lines
   1.335 +        if stripped[0] in comments:
   1.336 +            continue
   1.337 +
   1.338 +        # check for a new section
   1.339 +        if len(stripped) > 2 and stripped[0] == '[' and stripped[-1] == ']':
   1.340 +            section = stripped[1:-1].strip()
   1.341 +            key = value = None
   1.342 +
   1.343 +            # deal with DEFAULT section
   1.344 +            if section.lower() == default.lower():
   1.345 +                if strict:
   1.346 +                    assert default not in section_names
   1.347 +                section_names.add(default)
   1.348 +                current_section = variables
   1.349 +                continue
   1.350 +
   1.351 +            if strict:
   1.352 +                # make sure this section doesn't already exist
   1.353 +                assert section not in section_names, "Section '%s' already found in '%s'" % (section, section_names)
   1.354 +
   1.355 +            section_names.add(section)
   1.356 +            current_section = {}
   1.357 +            sections.append((section, current_section))
   1.358 +            continue
   1.359 +
   1.360 +        # if there aren't any sections yet, something bad happen
   1.361 +        if not section_names:
   1.362 +            raise Exception('No sections found')
   1.363 +
   1.364 +        # (key, value) pair
   1.365 +        for separator in separators:
   1.366 +            if separator in stripped:
   1.367 +                key, value = stripped.split(separator, 1)
   1.368 +                key = key.strip()
   1.369 +                value = value.strip()
   1.370 +
   1.371 +                if strict:
   1.372 +                    # make sure this key isn't already in the section or empty
   1.373 +                    assert key
   1.374 +                    if current_section is not variables:
   1.375 +                        assert key not in current_section
   1.376 +
   1.377 +                current_section[key] = value
   1.378 +                break
   1.379 +        else:
   1.380 +            # continuation line ?
   1.381 +            if line[0].isspace() and key:
   1.382 +                value = '%s%s%s' % (value, os.linesep, stripped)
   1.383 +                current_section[key] = value
   1.384 +            else:
   1.385 +                # something bad happened!
   1.386 +                if hasattr(fp, 'name'):
   1.387 +                    filename = fp.name
   1.388 +                else:
   1.389 +                    filename = 'unknown'
   1.390 +                raise Exception("Error parsing manifest file '%s', line %s" %
   1.391 +                                (filename, linenum))
   1.392 +
   1.393 +    # interpret the variables
   1.394 +    def interpret_variables(global_dict, local_dict):
   1.395 +        variables = global_dict.copy()
   1.396 +        if 'skip-if' in local_dict and 'skip-if' in variables:
   1.397 +            local_dict['skip-if'] = "(%s) || (%s)" % (variables['skip-if'].split('#')[0], local_dict['skip-if'].split('#')[0])
   1.398 +        variables.update(local_dict)
   1.399 +            
   1.400 +        return variables
   1.401 +
   1.402 +    sections = [(i, interpret_variables(variables, j)) for i, j in sections]
   1.403 +    return sections
   1.404 +
   1.405 +
   1.406 +### objects for parsing manifests
   1.407 +
   1.408 +class ManifestParser(object):
   1.409 +    """read .ini manifests"""
   1.410 +
   1.411 +    def __init__(self, manifests=(), defaults=None, strict=True):
   1.412 +        self._defaults = defaults or {}
   1.413 +        self.tests = []
   1.414 +        self.manifest_defaults = {}
   1.415 +        self.strict = strict
   1.416 +        self.rootdir = None
   1.417 +        self.relativeRoot = None
   1.418 +        if manifests:
   1.419 +            self.read(*manifests)
   1.420 +
   1.421 +    def getRelativeRoot(self, root):
   1.422 +        return root
   1.423 +
   1.424 +    ### methods for reading manifests
   1.425 +
   1.426 +    def _read(self, root, filename, defaults):
   1.427 +
   1.428 +        # get directory of this file if not file-like object
   1.429 +        if isinstance(filename, string):
   1.430 +            filename = os.path.abspath(filename)
   1.431 +            fp = open(filename)
   1.432 +            here = os.path.dirname(filename)
   1.433 +        else:
   1.434 +            fp = filename
   1.435 +            filename = here = None
   1.436 +        defaults['here'] = here
   1.437 +
   1.438 +        # Rootdir is needed for relative path calculation. Precompute it for
   1.439 +        # the microoptimization used below.
   1.440 +        if self.rootdir is None:
   1.441 +            rootdir = ""
   1.442 +        else:
   1.443 +            assert os.path.isabs(self.rootdir)
   1.444 +            rootdir = self.rootdir + os.path.sep
   1.445 +
   1.446 +        # read the configuration
   1.447 +        sections = read_ini(fp=fp, variables=defaults, strict=self.strict)
   1.448 +        self.manifest_defaults[filename] = defaults
   1.449 +
   1.450 +        # get the tests
   1.451 +        for section, data in sections:
   1.452 +            subsuite = ''
   1.453 +            if 'subsuite' in data:
   1.454 +                subsuite = data['subsuite']
   1.455 +
   1.456 +            # a file to include
   1.457 +            # TODO: keep track of included file structure:
   1.458 +            # self.manifests = {'manifest.ini': 'relative/path.ini'}
   1.459 +            if section.startswith('include:'):
   1.460 +                include_file = section.split('include:', 1)[-1]
   1.461 +                include_file = normalize_path(include_file)
   1.462 +                if not os.path.isabs(include_file):
   1.463 +                    include_file = os.path.join(self.getRelativeRoot(here), include_file)
   1.464 +                if not os.path.exists(include_file):
   1.465 +                    message = "Included file '%s' does not exist" % include_file
   1.466 +                    if self.strict:
   1.467 +                        raise IOError(message)
   1.468 +                    else:
   1.469 +                        sys.stderr.write("%s\n" % message)
   1.470 +                        continue
   1.471 +                include_defaults = data.copy()
   1.472 +                self._read(root, include_file, include_defaults)
   1.473 +                continue
   1.474 +
   1.475 +            # otherwise an item
   1.476 +            test = data
   1.477 +            test['name'] = section
   1.478 +
   1.479 +            # Will be None if the manifest being read is a file-like object.
   1.480 +            test['manifest'] = filename
   1.481 +
   1.482 +            # determine the path
   1.483 +            path = test.get('path', section)
   1.484 +            _relpath = path
   1.485 +            if '://' not in path: # don't futz with URLs
   1.486 +                path = normalize_path(path)
   1.487 +                if here and not os.path.isabs(path):
   1.488 +                    path = os.path.normpath(os.path.join(here, path))
   1.489 +
   1.490 +                # Microoptimization, because relpath is quite expensive.
   1.491 +                # We know that rootdir is an absolute path or empty. If path
   1.492 +                # starts with rootdir, then path is also absolute and the tail
   1.493 +                # of the path is the relative path (possibly non-normalized,
   1.494 +                # when here is unknown).
   1.495 +                # For this to work rootdir needs to be terminated with a path
   1.496 +                # separator, so that references to sibling directories with
   1.497 +                # a common prefix don't get misscomputed (e.g. /root and
   1.498 +                # /rootbeer/file).
   1.499 +                # When the rootdir is unknown, the relpath needs to be left
   1.500 +                # unchanged. We use an empty string as rootdir in that case,
   1.501 +                # which leaves relpath unchanged after slicing.
   1.502 +                if path.startswith(rootdir):
   1.503 +                    _relpath = path[len(rootdir):]
   1.504 +                else:
   1.505 +                    _relpath = relpath(path, rootdir)
   1.506 +
   1.507 +            test['subsuite'] = subsuite
   1.508 +            test['path'] = path
   1.509 +            test['relpath'] = _relpath
   1.510 +
   1.511 +            # append the item
   1.512 +            self.tests.append(test)
   1.513 +
   1.514 +    def read(self, *filenames, **defaults):
   1.515 +        """
   1.516 +        read and add manifests from file paths or file-like objects
   1.517 +
   1.518 +        filenames -- file paths or file-like objects to read as manifests
   1.519 +        defaults -- default variables
   1.520 +        """
   1.521 +
   1.522 +        # ensure all files exist
   1.523 +        missing = [filename for filename in filenames
   1.524 +                   if isinstance(filename, string) and not os.path.exists(filename) ]
   1.525 +        if missing:
   1.526 +            raise IOError('Missing files: %s' % ', '.join(missing))
   1.527 +
   1.528 +        # default variables
   1.529 +        _defaults = defaults.copy() or self._defaults.copy()
   1.530 +        _defaults.setdefault('here', None)
   1.531 +
   1.532 +        # process each file
   1.533 +        for filename in filenames:
   1.534 +            # set the per file defaults
   1.535 +            defaults = _defaults.copy()
   1.536 +            here = None
   1.537 +            if isinstance(filename, string):
   1.538 +                here = os.path.dirname(os.path.abspath(filename))
   1.539 +                defaults['here'] = here # directory of master .ini file
   1.540 +
   1.541 +            if self.rootdir is None:
   1.542 +                # set the root directory
   1.543 +                # == the directory of the first manifest given
   1.544 +                self.rootdir = here
   1.545 +
   1.546 +            self._read(here, filename, defaults)
   1.547 +
   1.548 +
   1.549 +    ### methods for querying manifests
   1.550 +
   1.551 +    def query(self, *checks, **kw):
   1.552 +        """
   1.553 +        general query function for tests
   1.554 +        - checks : callable conditions to test if the test fulfills the query
   1.555 +        """
   1.556 +        tests = kw.get('tests', None)
   1.557 +        if tests is None:
   1.558 +            tests = self.tests
   1.559 +        retval = []
   1.560 +        for test in tests:
   1.561 +            for check in checks:
   1.562 +                if not check(test):
   1.563 +                    break
   1.564 +            else:
   1.565 +                retval.append(test)
   1.566 +        return retval
   1.567 +
   1.568 +    def get(self, _key=None, inverse=False, tags=None, tests=None, **kwargs):
   1.569 +        # TODO: pass a dict instead of kwargs since you might hav
   1.570 +        # e.g. 'inverse' as a key in the dict
   1.571 +
   1.572 +        # TODO: tags should just be part of kwargs with None values
   1.573 +        # (None == any is kinda weird, but probably still better)
   1.574 +
   1.575 +        # fix up tags
   1.576 +        if tags:
   1.577 +            tags = set(tags)
   1.578 +        else:
   1.579 +            tags = set()
   1.580 +
   1.581 +        # make some check functions
   1.582 +        if inverse:
   1.583 +            has_tags = lambda test: not tags.intersection(test.keys())
   1.584 +            def dict_query(test):
   1.585 +                for key, value in kwargs.items():
   1.586 +                    if test.get(key) == value:
   1.587 +                        return False
   1.588 +                return True
   1.589 +        else:
   1.590 +            has_tags = lambda test: tags.issubset(test.keys())
   1.591 +            def dict_query(test):
   1.592 +                for key, value in kwargs.items():
   1.593 +                    if test.get(key) != value:
   1.594 +                        return False
   1.595 +                return True
   1.596 +
   1.597 +        # query the tests
   1.598 +        tests = self.query(has_tags, dict_query, tests=tests)
   1.599 +
   1.600 +        # if a key is given, return only a list of that key
   1.601 +        # useful for keys like 'name' or 'path'
   1.602 +        if _key:
   1.603 +            return [test[_key] for test in tests]
   1.604 +
   1.605 +        # return the tests
   1.606 +        return tests
   1.607 +
   1.608 +    def manifests(self, tests=None):
   1.609 +        """
   1.610 +        return manifests in order in which they appear in the tests
   1.611 +        """
   1.612 +        if tests is None:
   1.613 +            # Make sure to return all the manifests, even ones without tests.
   1.614 +            return self.manifest_defaults.keys()
   1.615 +
   1.616 +        manifests = []
   1.617 +        for test in tests:
   1.618 +            manifest = test.get('manifest')
   1.619 +            if not manifest:
   1.620 +                continue
   1.621 +            if manifest not in manifests:
   1.622 +                manifests.append(manifest)
   1.623 +        return manifests
   1.624 +
   1.625 +    def paths(self):
   1.626 +        return [i['path'] for i in self.tests]
   1.627 +
   1.628 +
   1.629 +    ### methods for auditing
   1.630 +
   1.631 +    def missing(self, tests=None):
   1.632 +        """return list of tests that do not exist on the filesystem"""
   1.633 +        if tests is None:
   1.634 +            tests = self.tests
   1.635 +        return [test for test in tests
   1.636 +                if not os.path.exists(test['path'])]
   1.637 +
   1.638 +    def verifyDirectory(self, directories, pattern=None, extensions=None):
   1.639 +        """
   1.640 +        checks what is on the filesystem vs what is in a manifest
   1.641 +        returns a 2-tuple of sets:
   1.642 +        (missing_from_filesystem, missing_from_manifest)
   1.643 +        """
   1.644 +
   1.645 +        files = set([])
   1.646 +        if isinstance(directories, basestring):
   1.647 +            directories = [directories]
   1.648 +
   1.649 +        # get files in directories
   1.650 +        for directory in directories:
   1.651 +            for dirpath, dirnames, filenames in os.walk(directory, topdown=True):
   1.652 +
   1.653 +                # only add files that match a pattern
   1.654 +                if pattern:
   1.655 +                    filenames = fnmatch.filter(filenames, pattern)
   1.656 +
   1.657 +                # only add files that have one of the extensions
   1.658 +                if extensions:
   1.659 +                    filenames = [filename for filename in filenames
   1.660 +                                 if os.path.splitext(filename)[-1] in extensions]
   1.661 +
   1.662 +                files.update([os.path.join(dirpath, filename) for filename in filenames])
   1.663 +
   1.664 +        paths = set(self.paths())
   1.665 +        missing_from_filesystem = paths.difference(files)
   1.666 +        missing_from_manifest = files.difference(paths)
   1.667 +        return (missing_from_filesystem, missing_from_manifest)
   1.668 +
   1.669 +
   1.670 +    ### methods for output
   1.671 +
   1.672 +    def write(self, fp=sys.stdout, rootdir=None,
   1.673 +              global_tags=None, global_kwargs=None,
   1.674 +              local_tags=None, local_kwargs=None):
   1.675 +        """
   1.676 +        write a manifest given a query
   1.677 +        global and local options will be munged to do the query
   1.678 +        globals will be written to the top of the file
   1.679 +        locals (if given) will be written per test
   1.680 +        """
   1.681 +
   1.682 +        # open file if `fp` given as string
   1.683 +        close = False
   1.684 +        if isinstance(fp, string):
   1.685 +            fp = file(fp, 'w')
   1.686 +            close = True
   1.687 +
   1.688 +        # root directory
   1.689 +        if rootdir is None:
   1.690 +            rootdir = self.rootdir
   1.691 +
   1.692 +        # sanitize input
   1.693 +        global_tags = global_tags or set()
   1.694 +        local_tags = local_tags or set()
   1.695 +        global_kwargs = global_kwargs or {}
   1.696 +        local_kwargs = local_kwargs or {}
   1.697 +
   1.698 +        # create the query
   1.699 +        tags = set([])
   1.700 +        tags.update(global_tags)
   1.701 +        tags.update(local_tags)
   1.702 +        kwargs = {}
   1.703 +        kwargs.update(global_kwargs)
   1.704 +        kwargs.update(local_kwargs)
   1.705 +
   1.706 +        # get matching tests
   1.707 +        tests = self.get(tags=tags, **kwargs)
   1.708 +
   1.709 +        # print the .ini manifest
   1.710 +        if global_tags or global_kwargs:
   1.711 +            print >> fp, '[DEFAULT]'
   1.712 +            for tag in global_tags:
   1.713 +                print >> fp, '%s =' % tag
   1.714 +            for key, value in global_kwargs.items():
   1.715 +                print >> fp, '%s = %s' % (key, value)
   1.716 +            print >> fp
   1.717 +
   1.718 +        for test in tests:
   1.719 +            test = test.copy() # don't overwrite
   1.720 +
   1.721 +            path = test['name']
   1.722 +            if not os.path.isabs(path):
   1.723 +                path = test['path']
   1.724 +                if self.rootdir:
   1.725 +                    path = relpath(test['path'], self.rootdir)
   1.726 +                path = denormalize_path(path)
   1.727 +            print >> fp, '[%s]' % path
   1.728 +
   1.729 +            # reserved keywords:
   1.730 +            reserved = ['path', 'name', 'here', 'manifest', 'relpath']
   1.731 +            for key in sorted(test.keys()):
   1.732 +                if key in reserved:
   1.733 +                    continue
   1.734 +                if key in global_kwargs:
   1.735 +                    continue
   1.736 +                if key in global_tags and not test[key]:
   1.737 +                    continue
   1.738 +                print >> fp, '%s = %s' % (key, test[key])
   1.739 +            print >> fp
   1.740 +
   1.741 +        if close:
   1.742 +            # close the created file
   1.743 +            fp.close()
   1.744 +
   1.745 +    def __str__(self):
   1.746 +        fp = StringIO()
   1.747 +        self.write(fp=fp)
   1.748 +        value = fp.getvalue()
   1.749 +        return value
   1.750 +
   1.751 +    def copy(self, directory, rootdir=None, *tags, **kwargs):
   1.752 +        """
   1.753 +        copy the manifests and associated tests
   1.754 +        - directory : directory to copy to
   1.755 +        - rootdir : root directory to copy to (if not given from manifests)
   1.756 +        - tags : keywords the tests must have
   1.757 +        - kwargs : key, values the tests must match
   1.758 +        """
   1.759 +        # XXX note that copy does *not* filter the tests out of the
   1.760 +        # resulting manifest; it just stupidly copies them over.
   1.761 +        # ideally, it would reread the manifests and filter out the
   1.762 +        # tests that don't match *tags and **kwargs
   1.763 +
   1.764 +        # destination
   1.765 +        if not os.path.exists(directory):
   1.766 +            os.path.makedirs(directory)
   1.767 +        else:
   1.768 +            # sanity check
   1.769 +            assert os.path.isdir(directory)
   1.770 +
   1.771 +        # tests to copy
   1.772 +        tests = self.get(tags=tags, **kwargs)
   1.773 +        if not tests:
   1.774 +            return # nothing to do!
   1.775 +
   1.776 +        # root directory
   1.777 +        if rootdir is None:
   1.778 +            rootdir = self.rootdir
   1.779 +
   1.780 +        # copy the manifests + tests
   1.781 +        manifests = [relpath(manifest, rootdir) for manifest in self.manifests()]
   1.782 +        for manifest in manifests:
   1.783 +            destination = os.path.join(directory, manifest)
   1.784 +            dirname = os.path.dirname(destination)
   1.785 +            if not os.path.exists(dirname):
   1.786 +                os.makedirs(dirname)
   1.787 +            else:
   1.788 +                # sanity check
   1.789 +                assert os.path.isdir(dirname)
   1.790 +            shutil.copy(os.path.join(rootdir, manifest), destination)
   1.791 +        for test in tests:
   1.792 +            if os.path.isabs(test['name']):
   1.793 +                continue
   1.794 +            source = test['path']
   1.795 +            if not os.path.exists(source):
   1.796 +                print >> sys.stderr, "Missing test: '%s' does not exist!" % source
   1.797 +                continue
   1.798 +                # TODO: should err on strict
   1.799 +            destination = os.path.join(directory, relpath(test['path'], rootdir))
   1.800 +            shutil.copy(source, destination)
   1.801 +            # TODO: ensure that all of the tests are below the from_dir
   1.802 +
   1.803 +    def update(self, from_dir, rootdir=None, *tags, **kwargs):
   1.804 +        """
   1.805 +        update the tests as listed in a manifest from a directory
   1.806 +        - from_dir : directory where the tests live
   1.807 +        - rootdir : root directory to copy to (if not given from manifests)
   1.808 +        - tags : keys the tests must have
   1.809 +        - kwargs : key, values the tests must match
   1.810 +        """
   1.811 +
   1.812 +        # get the tests
   1.813 +        tests = self.get(tags=tags, **kwargs)
   1.814 +
   1.815 +        # get the root directory
   1.816 +        if not rootdir:
   1.817 +            rootdir = self.rootdir
   1.818 +
   1.819 +        # copy them!
   1.820 +        for test in tests:
   1.821 +            if not os.path.isabs(test['name']):
   1.822 +                _relpath = relpath(test['path'], rootdir)
   1.823 +                source = os.path.join(from_dir, _relpath)
   1.824 +                if not os.path.exists(source):
   1.825 +                    # TODO err on strict
   1.826 +                    print >> sys.stderr, "Missing test: '%s'; skipping" % test['name']
   1.827 +                    continue
   1.828 +                destination = os.path.join(rootdir, _relpath)
   1.829 +                shutil.copy(source, destination)
   1.830 +
   1.831 +    ### directory importers
   1.832 +
   1.833 +    @classmethod
   1.834 +    def _walk_directories(cls, directories, function, pattern=None, ignore=()):
   1.835 +        """
   1.836 +        internal function to import directories
   1.837 +        """
   1.838 +
   1.839 +        class FilteredDirectoryContents(object):
   1.840 +            """class to filter directory contents"""
   1.841 +
   1.842 +            sort = sorted
   1.843 +
   1.844 +            def __init__(self, pattern=pattern, ignore=ignore, cache=None):
   1.845 +                if pattern is None:
   1.846 +                    pattern = set()
   1.847 +                if isinstance(pattern, basestring):
   1.848 +                    pattern = [pattern]
   1.849 +                self.patterns = pattern
   1.850 +                self.ignore = set(ignore)
   1.851 +
   1.852 +                # cache of (dirnames, filenames) keyed on directory real path
   1.853 +                # assumes volume is frozen throughout scope
   1.854 +                self._cache = cache or {}
   1.855 +
   1.856 +            def __call__(self, directory):
   1.857 +                """returns 2-tuple: dirnames, filenames"""
   1.858 +                directory = os.path.realpath(directory)
   1.859 +                if directory not in self._cache:
   1.860 +                    dirnames, filenames = self.contents(directory)
   1.861 +
   1.862 +                    # filter out directories without progeny
   1.863 +                    # XXX recursive: should keep track of seen directories
   1.864 +                    dirnames = [ dirname for dirname in dirnames
   1.865 +                                 if not self.empty(os.path.join(directory, dirname)) ]
   1.866 +
   1.867 +                    self._cache[directory] = (tuple(dirnames), filenames)
   1.868 +
   1.869 +                # return cached values
   1.870 +                return self._cache[directory]
   1.871 +
   1.872 +            def empty(self, directory):
   1.873 +                """
   1.874 +                returns if a directory and its descendents are empty
   1.875 +                """
   1.876 +                return self(directory) == ((), ())
   1.877 +
   1.878 +            def contents(self, directory, sort=None):
   1.879 +                """
   1.880 +                return directory contents as (dirnames, filenames)
   1.881 +                with `ignore` and `pattern` applied
   1.882 +                """
   1.883 +
   1.884 +                if sort is None:
   1.885 +                    sort = self.sort
   1.886 +
   1.887 +                # split directories and files
   1.888 +                dirnames = []
   1.889 +                filenames = []
   1.890 +                for item in os.listdir(directory):
   1.891 +                    path = os.path.join(directory, item)
   1.892 +                    if os.path.isdir(path):
   1.893 +                        dirnames.append(item)
   1.894 +                    else:
   1.895 +                        # XXX not sure what to do if neither a file or directory
   1.896 +                        # (if anything)
   1.897 +                        assert os.path.isfile(path)
   1.898 +                        filenames.append(item)
   1.899 +
   1.900 +                # filter contents;
   1.901 +                # this could be done in situ re the above for loop
   1.902 +                # but it is really disparate in intent
   1.903 +                # and could conceivably go to a separate method
   1.904 +                dirnames = [dirname for dirname in dirnames
   1.905 +                            if dirname not in self.ignore]
   1.906 +                filenames = set(filenames)
   1.907 +                # we use set functionality to filter filenames
   1.908 +                if self.patterns:
   1.909 +                    matches = set()
   1.910 +                    matches.update(*[fnmatch.filter(filenames, pattern)
   1.911 +                                     for pattern in self.patterns])
   1.912 +                    filenames = matches
   1.913 +
   1.914 +                if sort is not None:
   1.915 +                    # sort dirnames, filenames
   1.916 +                    dirnames = sort(dirnames)
   1.917 +                    filenames = sort(filenames)
   1.918 +
   1.919 +                return (tuple(dirnames), tuple(filenames))
   1.920 +
   1.921 +        # make a filtered directory object
   1.922 +        directory_contents = FilteredDirectoryContents(pattern=pattern, ignore=ignore)
   1.923 +
   1.924 +        # walk the directories, generating manifests
   1.925 +        for index, directory in enumerate(directories):
   1.926 +
   1.927 +            for dirpath, dirnames, filenames in os.walk(directory):
   1.928 +
   1.929 +                # get the directory contents from the caching object
   1.930 +                _dirnames, filenames = directory_contents(dirpath)
   1.931 +                # filter out directory names
   1.932 +                dirnames[:] = _dirnames
   1.933 +
   1.934 +                # call callback function
   1.935 +                function(directory, dirpath, dirnames, filenames)
   1.936 +
   1.937 +    @classmethod
   1.938 +    def populate_directory_manifests(cls, directories, filename, pattern=None, ignore=(), overwrite=False):
   1.939 +        """
   1.940 +        walks directories and writes manifests of name `filename` in-place; returns `cls` instance populated
   1.941 +        with the given manifests
   1.942 +
   1.943 +        filename -- filename of manifests to write
   1.944 +        pattern -- shell pattern (glob) or patterns of filenames to match
   1.945 +        ignore -- directory names to ignore
   1.946 +        overwrite -- whether to overwrite existing files of given name
   1.947 +        """
   1.948 +
   1.949 +        manifest_dict = {}
   1.950 +        seen = [] # top-level directories seen
   1.951 +
   1.952 +        if os.path.basename(filename) != filename:
   1.953 +            raise IOError("filename should not include directory name")
   1.954 +
   1.955 +        # no need to hit directories more than once
   1.956 +        _directories = directories
   1.957 +        directories = []
   1.958 +        for directory in _directories:
   1.959 +            if directory not in directories:
   1.960 +                directories.append(directory)
   1.961 +
   1.962 +        def callback(directory, dirpath, dirnames, filenames):
   1.963 +            """write a manifest for each directory"""
   1.964 +
   1.965 +            manifest_path = os.path.join(dirpath, filename)
   1.966 +            if (dirnames or filenames) and not (os.path.exists(manifest_path) and overwrite):
   1.967 +                with file(manifest_path, 'w') as manifest:
   1.968 +                    for dirname in dirnames:
   1.969 +                        print >> manifest, '[include:%s]' % os.path.join(dirname, filename)
   1.970 +                    for _filename in filenames:
   1.971 +                        print >> manifest, '[%s]' % _filename
   1.972 +
   1.973 +                # add to list of manifests
   1.974 +                manifest_dict.setdefault(directory, manifest_path)
   1.975 +
   1.976 +        # walk the directories to gather files
   1.977 +        cls._walk_directories(directories, callback, pattern=pattern, ignore=ignore)
   1.978 +        # get manifests
   1.979 +        manifests = [manifest_dict[directory] for directory in _directories]
   1.980 +
   1.981 +        # create a `cls` instance with the manifests
   1.982 +        return cls(manifests=manifests)
   1.983 +
   1.984 +    @classmethod
   1.985 +    def from_directories(cls, directories, pattern=None, ignore=(), write=None, relative_to=None):
   1.986 +        """
   1.987 +        convert directories to a simple manifest; returns ManifestParser instance
   1.988 +
   1.989 +        pattern -- shell pattern (glob) or patterns of filenames to match
   1.990 +        ignore -- directory names to ignore
   1.991 +        write -- filename or file-like object of manifests to write;
   1.992 +                 if `None` then a StringIO instance will be created
   1.993 +        relative_to -- write paths relative to this path;
   1.994 +                       if false then the paths are absolute
   1.995 +        """
   1.996 +
   1.997 +
   1.998 +        # determine output
   1.999 +        opened_manifest_file = None # name of opened manifest file
  1.1000 +        absolute = not relative_to # whether to output absolute path names as names
  1.1001 +        if isinstance(write, string):
  1.1002 +            opened_manifest_file = write
  1.1003 +            write = file(write, 'w')
  1.1004 +        if write is None:
  1.1005 +            write = StringIO()
  1.1006 +
  1.1007 +        # walk the directories, generating manifests
  1.1008 +        def callback(directory, dirpath, dirnames, filenames):
  1.1009 +
  1.1010 +            # absolute paths
  1.1011 +            filenames = [os.path.join(dirpath, filename)
  1.1012 +                         for filename in filenames]
  1.1013 +            # ensure new manifest isn't added
  1.1014 +            filenames = [filename for filename in filenames
  1.1015 +                         if filename != opened_manifest_file]
  1.1016 +            # normalize paths
  1.1017 +            if not absolute and relative_to:
  1.1018 +                filenames = [relpath(filename, relative_to)
  1.1019 +                             for filename in filenames]
  1.1020 +
  1.1021 +            # write to manifest
  1.1022 +            print >> write, '\n'.join(['[%s]' % denormalize_path(filename)
  1.1023 +                                               for filename in filenames])
  1.1024 +
  1.1025 +
  1.1026 +        cls._walk_directories(directories, callback, pattern=pattern, ignore=ignore)
  1.1027 +
  1.1028 +        if opened_manifest_file:
  1.1029 +            # close file
  1.1030 +            write.close()
  1.1031 +            manifests = [opened_manifest_file]
  1.1032 +        else:
  1.1033 +            # manifests/write is a file-like object;
  1.1034 +            # rewind buffer
  1.1035 +            write.flush()
  1.1036 +            write.seek(0)
  1.1037 +            manifests = [write]
  1.1038 +
  1.1039 +
  1.1040 +        # make a ManifestParser instance
  1.1041 +        return cls(manifests=manifests)
  1.1042 +
  1.1043 +convert = ManifestParser.from_directories
  1.1044 +
  1.1045 +
  1.1046 +class TestManifest(ManifestParser):
  1.1047 +    """
  1.1048 +    apply logic to manifests;  this is your integration layer :)
  1.1049 +    specific harnesses may subclass from this if they need more logic
  1.1050 +    """
  1.1051 +
  1.1052 +    def filter(self, values, tests):
  1.1053 +        """
  1.1054 +        filter on a specific list tag, e.g.:
  1.1055 +        run-if = os == win linux
  1.1056 +        skip-if = os == mac
  1.1057 +        """
  1.1058 +
  1.1059 +        # tags:
  1.1060 +        run_tag = 'run-if'
  1.1061 +        skip_tag = 'skip-if'
  1.1062 +        fail_tag = 'fail-if'
  1.1063 +
  1.1064 +        # loop over test
  1.1065 +        for test in tests:
  1.1066 +            reason = None # reason to disable
  1.1067 +
  1.1068 +            # tagged-values to run
  1.1069 +            if run_tag in test:
  1.1070 +                condition = test[run_tag]
  1.1071 +                if not parse(condition, **values):
  1.1072 +                    reason = '%s: %s' % (run_tag, condition)
  1.1073 +
  1.1074 +            # tagged-values to skip
  1.1075 +            if skip_tag in test:
  1.1076 +                condition = test[skip_tag]
  1.1077 +                if parse(condition, **values):
  1.1078 +                    reason = '%s: %s' % (skip_tag, condition)
  1.1079 +
  1.1080 +            # mark test as disabled if there's a reason
  1.1081 +            if reason:
  1.1082 +                test.setdefault('disabled', reason)
  1.1083 +
  1.1084 +            # mark test as a fail if so indicated
  1.1085 +            if fail_tag in test:
  1.1086 +                condition = test[fail_tag]
  1.1087 +                if parse(condition, **values):
  1.1088 +                    test['expected'] = 'fail'
  1.1089 +
  1.1090 +    def active_tests(self, exists=True, disabled=True, options=None, **values):
  1.1091 +        """
  1.1092 +        - exists : return only existing tests
  1.1093 +        - disabled : whether to return disabled tests
  1.1094 +        - tags : keys and values to filter on (e.g. `os = linux mac`)
  1.1095 +        """
  1.1096 +        tests = [i.copy() for i in self.tests] # shallow copy
  1.1097 +
  1.1098 +        # Filter on current subsuite
  1.1099 +        if options:
  1.1100 +            if  options.subsuite:
  1.1101 +                tests = [test for test in tests if options.subsuite == test['subsuite']]
  1.1102 +            else:
  1.1103 +                tests = [test for test in tests if not test['subsuite']]
  1.1104 +
  1.1105 +        # mark all tests as passing unless indicated otherwise
  1.1106 +        for test in tests:
  1.1107 +            test['expected'] = test.get('expected', 'pass')
  1.1108 +
  1.1109 +        # ignore tests that do not exist
  1.1110 +        if exists:
  1.1111 +            tests = [test for test in tests if os.path.exists(test['path'])]
  1.1112 +
  1.1113 +        # filter by tags
  1.1114 +        self.filter(values, tests)
  1.1115 +
  1.1116 +        # ignore disabled tests if specified
  1.1117 +        if not disabled:
  1.1118 +            tests = [test for test in tests
  1.1119 +                     if not 'disabled' in test]
  1.1120 +
  1.1121 +        # return active tests
  1.1122 +        return tests
  1.1123 +
  1.1124 +    def test_paths(self):
  1.1125 +        return [test['path'] for test in self.active_tests()]
  1.1126 +
  1.1127 +
  1.1128 +### command line attributes
  1.1129 +
  1.1130 +class ParserError(Exception):
  1.1131 +  """error for exceptions while parsing the command line"""
  1.1132 +
  1.1133 +def parse_args(_args):
  1.1134 +    """
  1.1135 +    parse and return:
  1.1136 +    --keys=value (or --key value)
  1.1137 +    -tags
  1.1138 +    args
  1.1139 +    """
  1.1140 +
  1.1141 +    # return values
  1.1142 +    _dict = {}
  1.1143 +    tags = []
  1.1144 +    args = []
  1.1145 +
  1.1146 +    # parse the arguments
  1.1147 +    key = None
  1.1148 +    for arg in _args:
  1.1149 +        if arg.startswith('---'):
  1.1150 +            raise ParserError("arguments should start with '-' or '--' only")
  1.1151 +        elif arg.startswith('--'):
  1.1152 +            if key:
  1.1153 +                raise ParserError("Key %s still open" % key)
  1.1154 +            key = arg[2:]
  1.1155 +            if '=' in key:
  1.1156 +                key, value = key.split('=', 1)
  1.1157 +                _dict[key] = value
  1.1158 +                key = None
  1.1159 +                continue
  1.1160 +        elif arg.startswith('-'):
  1.1161 +            if key:
  1.1162 +                raise ParserError("Key %s still open" % key)
  1.1163 +            tags.append(arg[1:])
  1.1164 +            continue
  1.1165 +        else:
  1.1166 +            if key:
  1.1167 +                _dict[key] = arg
  1.1168 +                continue
  1.1169 +            args.append(arg)
  1.1170 +
  1.1171 +    # return values
  1.1172 +    return (_dict, tags, args)
  1.1173 +
  1.1174 +
  1.1175 +### classes for subcommands
  1.1176 +
  1.1177 +class CLICommand(object):
  1.1178 +    usage = '%prog [options] command'
  1.1179 +    def __init__(self, parser):
  1.1180 +      self._parser = parser # master parser
  1.1181 +    def parser(self):
  1.1182 +      return OptionParser(usage=self.usage, description=self.__doc__,
  1.1183 +                          add_help_option=False)
  1.1184 +
  1.1185 +class Copy(CLICommand):
  1.1186 +    usage = '%prog [options] copy manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...'
  1.1187 +    def __call__(self, options, args):
  1.1188 +      # parse the arguments
  1.1189 +      try:
  1.1190 +        kwargs, tags, args = parse_args(args)
  1.1191 +      except ParserError, e:
  1.1192 +        self._parser.error(e.message)
  1.1193 +
  1.1194 +      # make sure we have some manifests, otherwise it will
  1.1195 +      # be quite boring
  1.1196 +      if not len(args) == 2:
  1.1197 +        HelpCLI(self._parser)(options, ['copy'])
  1.1198 +        return
  1.1199 +
  1.1200 +      # read the manifests
  1.1201 +      # TODO: should probably ensure these exist here
  1.1202 +      manifests = ManifestParser()
  1.1203 +      manifests.read(args[0])
  1.1204 +
  1.1205 +      # print the resultant query
  1.1206 +      manifests.copy(args[1], None, *tags, **kwargs)
  1.1207 +
  1.1208 +
  1.1209 +class CreateCLI(CLICommand):
  1.1210 +    """
  1.1211 +    create a manifest from a list of directories
  1.1212 +    """
  1.1213 +    usage = '%prog [options] create directory <directory> <...>'
  1.1214 +
  1.1215 +    def parser(self):
  1.1216 +        parser = CLICommand.parser(self)
  1.1217 +        parser.add_option('-p', '--pattern', dest='pattern',
  1.1218 +                          help="glob pattern for files")
  1.1219 +        parser.add_option('-i', '--ignore', dest='ignore',
  1.1220 +                          default=[], action='append',
  1.1221 +                          help='directories to ignore')
  1.1222 +        parser.add_option('-w', '--in-place', dest='in_place',
  1.1223 +                          help='Write .ini files in place; filename to write to')
  1.1224 +        return parser
  1.1225 +
  1.1226 +    def __call__(self, _options, args):
  1.1227 +        parser = self.parser()
  1.1228 +        options, args = parser.parse_args(args)
  1.1229 +
  1.1230 +        # need some directories
  1.1231 +        if not len(args):
  1.1232 +            parser.print_usage()
  1.1233 +            return
  1.1234 +
  1.1235 +        # add the directories to the manifest
  1.1236 +        for arg in args:
  1.1237 +            assert os.path.exists(arg)
  1.1238 +            assert os.path.isdir(arg)
  1.1239 +            manifest = convert(args, pattern=options.pattern, ignore=options.ignore,
  1.1240 +                               write=options.in_place)
  1.1241 +        if manifest:
  1.1242 +            print manifest
  1.1243 +
  1.1244 +
  1.1245 +class WriteCLI(CLICommand):
  1.1246 +    """
  1.1247 +    write a manifest based on a query
  1.1248 +    """
  1.1249 +    usage = '%prog [options] write manifest <manifest> -tag1 -tag2 --key1=value1 --key2=value2 ...'
  1.1250 +    def __call__(self, options, args):
  1.1251 +
  1.1252 +        # parse the arguments
  1.1253 +        try:
  1.1254 +            kwargs, tags, args = parse_args(args)
  1.1255 +        except ParserError, e:
  1.1256 +            self._parser.error(e.message)
  1.1257 +
  1.1258 +        # make sure we have some manifests, otherwise it will
  1.1259 +        # be quite boring
  1.1260 +        if not args:
  1.1261 +            HelpCLI(self._parser)(options, ['write'])
  1.1262 +            return
  1.1263 +
  1.1264 +        # read the manifests
  1.1265 +        # TODO: should probably ensure these exist here
  1.1266 +        manifests = ManifestParser()
  1.1267 +        manifests.read(*args)
  1.1268 +
  1.1269 +        # print the resultant query
  1.1270 +        manifests.write(global_tags=tags, global_kwargs=kwargs)
  1.1271 +
  1.1272 +
  1.1273 +class HelpCLI(CLICommand):
  1.1274 +    """
  1.1275 +    get help on a command
  1.1276 +    """
  1.1277 +    usage = '%prog [options] help [command]'
  1.1278 +
  1.1279 +    def __call__(self, options, args):
  1.1280 +        if len(args) == 1 and args[0] in commands:
  1.1281 +            commands[args[0]](self._parser).parser().print_help()
  1.1282 +        else:
  1.1283 +            self._parser.print_help()
  1.1284 +            print '\nCommands:'
  1.1285 +            for command in sorted(commands):
  1.1286 +                print '  %s : %s' % (command, commands[command].__doc__.strip())
  1.1287 +
  1.1288 +class UpdateCLI(CLICommand):
  1.1289 +    """
  1.1290 +    update the tests as listed in a manifest from a directory
  1.1291 +    """
  1.1292 +    usage = '%prog [options] update manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...'
  1.1293 +
  1.1294 +    def __call__(self, options, args):
  1.1295 +        # parse the arguments
  1.1296 +        try:
  1.1297 +            kwargs, tags, args = parse_args(args)
  1.1298 +        except ParserError, e:
  1.1299 +            self._parser.error(e.message)
  1.1300 +
  1.1301 +        # make sure we have some manifests, otherwise it will
  1.1302 +        # be quite boring
  1.1303 +        if not len(args) == 2:
  1.1304 +            HelpCLI(self._parser)(options, ['update'])
  1.1305 +            return
  1.1306 +
  1.1307 +        # read the manifests
  1.1308 +        # TODO: should probably ensure these exist here
  1.1309 +        manifests = ManifestParser()
  1.1310 +        manifests.read(args[0])
  1.1311 +
  1.1312 +        # print the resultant query
  1.1313 +        manifests.update(args[1], None, *tags, **kwargs)
  1.1314 +
  1.1315 +
  1.1316 +# command -> class mapping
  1.1317 +commands = { 'create': CreateCLI,
  1.1318 +             'help': HelpCLI,
  1.1319 +             'update': UpdateCLI,
  1.1320 +             'write': WriteCLI }
  1.1321 +
  1.1322 +def main(args=sys.argv[1:]):
  1.1323 +    """console_script entry point"""
  1.1324 +
  1.1325 +    # set up an option parser
  1.1326 +    usage = '%prog [options] [command] ...'
  1.1327 +    description = "%s. Use `help` to display commands" % __doc__.strip()
  1.1328 +    parser = OptionParser(usage=usage, description=description)
  1.1329 +    parser.add_option('-s', '--strict', dest='strict',
  1.1330 +                      action='store_true', default=False,
  1.1331 +                      help='adhere strictly to errors')
  1.1332 +    parser.disable_interspersed_args()
  1.1333 +
  1.1334 +    options, args = parser.parse_args(args)
  1.1335 +
  1.1336 +    if not args:
  1.1337 +        HelpCLI(parser)(options, args)
  1.1338 +        parser.exit()
  1.1339 +
  1.1340 +    # get the command
  1.1341 +    command = args[0]
  1.1342 +    if command not in commands:
  1.1343 +        parser.error("Command must be one of %s (you gave '%s')" % (', '.join(sorted(commands.keys())), command))
  1.1344 +
  1.1345 +    handler = commands[command](parser)
  1.1346 +    handler(options, args[1:])
  1.1347 +
  1.1348 +if __name__ == '__main__':
  1.1349 +    main()

mercurial