~ubuntu-branches/ubuntu/trusty/enigmail/trusty

« back to all changes in this revision

Viewing changes to mozilla/testing/mozbase/manifestdestiny/manifestparser/manifestparser.py

  • Committer: Package Import Robot
  • Author(s): Chris Coulson
  • Date: 2013-09-13 16:02:15 UTC
  • mfrom: (0.12.16)
  • Revision ID: package-import@ubuntu.com-20130913160215-mpeaob8bhtk42aun
Tags: 2:1.5.2-0ubuntu1
* New upstream release v1.5.2 for Thunderbird 24

* Build enigmail using a stripped down Thunderbird 17 build system, as it's
  now quite difficult to build the way we were doing previously, with the
  latest Firefox build system
* Add debian/patches/no_libxpcom.patch - Don't link against libxpcom, as it
  doesn't exist anymore (but exists in the build system)
* Add debian/patches/use_sdk.patch - Use the SDK version of xpt.py and
  friends
* Drop debian/patches/ipc-pipe_rename.diff (not needed anymore)
* Drop debian/patches/makefile_depth.diff (not needed anymore)

Show diffs side-by-side

added added

removed removed

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