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.

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

mercurial