Bug 616999. Xpcshell manifest support; harness changes. r=ted, a=test-only
authorJoel Maher <jmaher@mozilla.com>
Fri, 20 May 2011 11:54:01 -0400
changeset 70130 40195c0187d3069b848b012065476ab8868e6d0c
parent 70129 a365ca6c2379320357bf8ffa54006a916d02ed7d
child 70131 9e2f8321f4800c3ff03f26bda29930afba94975a
push id11
push userffxbld
push dateThu, 11 Aug 2011 21:43:38 +0000
treeherdermozilla-release@cf0a29826586 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersted, test-only
bugs616999
milestone6.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 616999. Xpcshell manifest support; harness changes. r=ted, a=test-only
build/manifestparser.py
config/rules.mk
js/src/config/rules.mk
services/crypto/component/tests/Makefile.in
testing/testsuite-targets.mk
testing/xpcshell/Makefile.in
testing/xpcshell/runxpcshelltests.py
toolkit/mozapps/extensions/test/Makefile.in
new file mode 100755
--- /dev/null
+++ b/build/manifestparser.py
@@ -0,0 +1,851 @@
+#!/usr/bin/env python
+
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+# 
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+# 
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+# 
+# The Original Code is mozilla.org code.
+# 
+# The Initial Developer of the Original Code is
+# Mozilla.org.
+# Portions created by the Initial Developer are Copyright (C) 2010
+# the Initial Developer. All Rights Reserved.
+# 
+# Contributor(s):
+#     Jeff Hammel <jhammel@mozilla.com>     (Original author)
+# 
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+# 
+# ***** END LICENSE BLOCK *****
+
+"""
+Mozilla universal manifest parser
+"""
+
+# this file lives at
+# http://hg.mozilla.org/automation/ManifestDestiny/raw-file/tip/manifestparser.py
+
+__all__ = ['ManifestParser', 'TestManifest', 'convert']
+
+import os
+import shutil
+import sys
+from fnmatch import fnmatch
+from optparse import OptionParser
+
+version = '0.3.1' # package version
+try:
+    from setuptools import setup
+except ImportError:
+    setup = None
+
+def normalize_path(path):
+    """normalize a relative path"""
+    if sys.platform.startswith('win'):
+        return path.replace('/', os.path.sep)
+    return path
+
+def read_ini(fp, variables=None, default='DEFAULT',
+             comments=';#', separators=('=', ':'),
+             strict=True):
+    """
+    read an .ini file and return a list of [(section, values)]
+    - fp : file pointer or path to read
+    - variables : default set of variables
+    - default : name of the section for the default section
+    - comments : characters that if they start a line denote a comment
+    - separators : strings that denote key, value separation in order
+    - strict : whether to be strict about parsing
+    """
+
+    if variables is None:
+        variables = {}
+
+    if isinstance(fp, basestring):
+        fp = file(fp)
+
+    sections = []
+    key = value = None
+    section_names = set([])
+
+    # read the lines
+    for line in fp.readlines():
+
+        stripped = line.strip()
+
+        # ignore blank lines
+        if not stripped:
+            # reset key and value to avoid continuation lines
+            key = value = None
+            continue
+
+        # ignore comment lines
+        if stripped[0] in comments:
+            continue
+
+        # check for a new section
+        if len(stripped) > 2 and stripped[0] == '[' and stripped[-1] == ']':
+            section = stripped[1:-1].strip()
+            key = value = None
+
+            # deal with DEFAULT section
+            if section.lower() == default.lower():
+                if strict:
+                    assert default not in section_names
+                section_names.add(default)
+                current_section = variables
+                continue
+
+            if strict:
+                # make sure this section doesn't already exist
+                assert section not in section_names
+
+            section_names.add(section)
+            current_section = {}
+            sections.append((section, current_section))
+            continue
+
+        # if there aren't any sections yet, something bad happen
+        if not section_names:
+            raise Exception('No sections yet :(')
+
+        # (key, value) pair
+        for separator in separators:
+            if separator in stripped:
+                key, value = stripped.split(separator, 1)
+                key = key.strip()
+                value = value.strip()
+
+                if strict:
+                    # make sure this key isn't already in the section or empty
+                    assert key
+                    if current_section is not variables:
+                        assert key not in current_section
+
+                current_section[key] = value
+                break
+        else:
+            # continuation line ?
+            if line[0].isspace() and key:
+                value = '%s%s%s' % (value, os.linesep, stripped)
+                current_section[key] = value
+            else:
+                # something bad happen!
+                raise Exception("Not sure what you're trying to do")
+
+    # interpret the variables
+    def interpret_variables(global_dict, local_dict):
+        variables = global_dict.copy()
+        variables.update(local_dict)
+        return variables
+
+    sections = [(i, interpret_variables(variables, j)) for i, j in sections]
+    return sections
+
+
+### objects for parsing manifests
+
+class ManifestParser(object):
+    """read .ini manifests"""
+
+    ### methods for reading manifests
+
+    def __init__(self, manifests=(), defaults=None, strict=True):
+        self._defaults = defaults or {}
+        self.tests = []
+        self.strict = strict
+        self.rootdir = None
+        self.relativeRoot = None
+        if manifests:
+            self.read(*manifests)
+
+    def getRelativeRoot(self):
+        return self.relativeRoot
+
+    def read(self, *filenames, **defaults):
+
+        # ensure all files exist
+        missing = [ filename for filename in filenames
+                    if not os.path.exists(filename) ]
+        if missing:
+            raise IOError('Missing files: %s' % ', '.join(missing))
+
+        # process each file
+        for filename in filenames:
+
+            # set the per file defaults
+            defaults = defaults.copy() or self._defaults.copy()
+            here = os.path.dirname(os.path.abspath(filename))
+            self.relativeRoot = here
+            defaults['here'] = here
+
+            if self.rootdir is None:
+                # set the root directory
+                # == the directory of the first manifest given
+                self.rootdir = here
+
+            # read the configuration
+            sections = read_ini(fp=filename, variables=defaults, strict=self.strict)
+
+            # get the tests
+            for section, data in sections:
+
+                # a file to include
+                # TODO: keep track of included file structure:
+                # self.manifests = {'manifest.ini': 'relative/path.ini'}
+                if section.startswith('include:'):
+                    include_file = section.split('include:', 1)[-1]
+                    include_file = normalize_path(include_file)
+                    if not os.path.isabs(include_file):
+                        include_file = os.path.join(self.getRelativeRoot(), include_file)
+                    if not os.path.exists(include_file):
+                        if self.strict:
+                            raise IOError("File '%s' does not exist" % include_file)
+                        else:
+                            continue
+                    include_defaults = data.copy()
+                    self.read(include_file, **include_defaults)
+                    continue
+
+                # otherwise an item
+                test = data
+                test['name'] = section
+                test['manifest'] = os.path.abspath(filename)
+
+                # determine the path
+                path = test.get('path', section)
+                if '://' not in path: # don't futz with URLs
+                    path = normalize_path(path)
+                    if not os.path.isabs(path):
+                        path = os.path.join(here, path)
+                test['path'] = path
+
+                # append the item
+                self.tests.append(test)
+
+    ### methods for querying manifests
+
+    def query(self, *checks):
+        """
+        general query function for tests
+        - checks : callable conditions to test if the test fulfills the query
+        """
+        retval = []
+        for test in self.tests:
+            for check in checks:
+                if not check(test):
+                    break
+            else:
+                retval.append(test)
+        return retval
+
+    def get(self, _key=None, inverse=False, tags=None, **kwargs):
+        # TODO: pass a dict instead of kwargs since you might hav
+        # e.g. 'inverse' as a key in the dict
+
+        # TODO: tags should just be part of kwargs with None values
+        # (None == any is kinda weird, but probably still better)
+
+        # fix up tags
+        if tags:
+            tags = set(tags)
+        else:
+            tags = set()
+
+        # make some check functions
+        if inverse:
+            has_tags = lambda test: tags.isdisjoint(test.keys())
+            def dict_query(test):
+                for key, value in kwargs.items():
+                    if test.get(key) == value:
+                        return False
+                return True
+        else:
+            has_tags = lambda test: tags.issubset(test.keys())
+            def dict_query(test):
+                for key, value in kwargs.items():
+                    if test.get(key) != value:
+                        return False
+                return True
+
+        # query the tests
+        tests = self.query(has_tags, dict_query)
+
+        # if a key is given, return only a list of that key
+        # useful for keys like 'name' or 'path'
+        if _key:
+            return [test[_key] for test in tests]
+
+        # return the tests
+        return tests
+
+    def missing(self, tests=None):
+        """return list of tests that do not exist on the filesystem"""
+        if tests is None:
+            tests = self.tests
+        return [test for test in tests
+                if not os.path.exists(test['path'])]
+
+    def manifests(self, tests=None):
+        """
+        return manifests in order in which they appear in the tests
+        """
+        if tests is None:
+            tests = self.tests
+        manifests = []
+        for test in tests:
+            manifest = test.get('manifest')
+            if not manifest:
+                continue
+            if manifest not in manifests:
+                manifests.append(manifest)
+        return manifests
+
+    ### methods for outputting from manifests
+
+    def write(self, fp=sys.stdout, rootdir=None,
+              global_tags=None, global_kwargs=None,
+              local_tags=None, local_kwargs=None):
+        """
+        write a manifest given a query
+        global and local options will be munged to do the query
+        globals will be written to the top of the file
+        locals (if given) will be written per test
+        """
+
+        # root directory
+        if rootdir is None:
+            rootdir = self.rootdir
+
+        # sanitize input
+        global_tags = global_tags or set()
+        local_tags = local_tags or set()
+        global_kwargs = global_kwargs or {}
+        local_kwargs = local_kwargs or {}
+        
+        # create the query
+        tags = set([])
+        tags.update(global_tags)
+        tags.update(local_tags)
+        kwargs = {}
+        kwargs.update(global_kwargs)
+        kwargs.update(local_kwargs)
+
+        # get matching tests
+        tests = self.get(tags=tags, **kwargs)
+
+        # print the .ini manifest
+        if global_tags or global_kwargs:
+            print >> fp, '[DEFAULT]'
+            for tag in global_tags:
+                print >> fp, '%s =' % tag
+            for key, value in global_kwargs.items():
+                print >> fp, '%s = %s' % (key, value)
+            print >> fp
+
+        for test in tests:
+            test = test.copy() # don't overwrite
+
+            path = test['name']
+            if not os.path.isabs(path):
+                path = os.path.relpath(test['path'], self.rootdir)
+            print >> fp, '[%s]' % path
+          
+            # reserved keywords:
+            reserved = ['path', 'name', 'here', 'manifest']
+            for key in sorted(test.keys()):
+                if key in reserved:
+                    continue
+                if key in global_kwargs:
+                    continue
+                if key in global_tags and not test[key]:
+                    continue
+                print >> fp, '%s = %s' % (key, test[key])
+            print >> fp
+
+    def copy(self, directory, rootdir=None, *tags, **kwargs):
+        """
+        copy the manifests and associated tests
+        - directory : directory to copy to
+        - rootdir : root directory to copy to (if not given from manifests)
+        - tags : keywords the tests must have
+        - kwargs : key, values the tests must match
+        """
+        # XXX note that copy does *not* filter the tests out of the
+        # resulting manifest; it just stupidly copies them over.
+        # ideally, it would reread the manifests and filter out the
+        # tests that don't match *tags and **kwargs
+        
+        # destination
+        if not os.path.exists(directory):
+            os.path.makedirs(directory)
+        else:
+            # sanity check
+            assert os.path.isdir(directory)
+
+        # tests to copy
+        tests = self.get(tags=tags, **kwargs)
+        if not tests:
+            return # nothing to do!
+
+        # root directory
+        if rootdir is None:
+            rootdir = self.rootdir
+
+        # copy the manifests + tests
+        manifests = [os.path.relpath(manifest, rootdir) for manifest in self.manifests()]
+        for manifest in manifests:
+            destination = os.path.join(directory, manifest)
+            dirname = os.path.dirname(destination)
+            if not os.path.exists(dirname):
+                os.makedirs(dirname)
+            else:
+                # sanity check
+                assert os.path.isdir(dirname)
+            shutil.copy(os.path.join(rootdir, manifest), destination)
+        for test in tests:
+            if os.path.isabs(test['name']):
+                continue
+            source = test['path']
+            if not os.path.exists(source):
+                print >> sys.stderr, "Missing test: '%s' does not exist!" % source
+                continue
+                # TODO: should err on strict
+            destination = os.path.join(directory, os.path.relpath(test['path'], rootdir))
+            shutil.copy(source, destination)
+            # TODO: ensure that all of the tests are below the from_dir
+
+    def update(self, from_dir, rootdir=None, *tags, **kwargs):
+        """
+        update the tests as listed in a manifest from a directory
+        - from_dir : directory where the tests live
+        - rootdir : root directory to copy to (if not given from manifests)
+        - tags : keys the tests must have
+        - kwargs : key, values the tests must match
+        """
+    
+        # get the tests
+        tests = self.get(tags=tags, **kwargs)
+
+        # get the root directory
+        if not rootdir:
+            rootdir = self.rootdir
+
+        # copy them!
+        for test in tests:
+            if not os.path.isabs(test['name']):
+                relpath = os.path.relpath(test['path'], rootdir)
+                source = os.path.join(from_dir, relpath)
+                if not os.path.exists(source):
+                    # TODO err on strict
+                    print >> sys.stderr, "Missing test: '%s'; skipping" % test['name']
+                    continue
+                destination = os.path.join(rootdir, relpath)
+                shutil.copy(source, destination)
+
+
+class TestManifest(ManifestParser):
+    """
+    apply logic to manifests;  this is your integration layer :)
+    specific harnesses may subclass from this if they need more logic
+    """
+
+    def filter(self, tag, value, tests=None):
+        """
+        filter on a specific list tag, e.g.:
+        run-if.os = win linux
+        skip-if.os = mac
+        """
+
+        if tests is None:
+            tests = self.tests
+        
+        # tags:
+        run_tag = 'run-if.' + tag
+        skip_tag = 'skip-if.' + tag        
+
+        # loop over test
+        for test in tests:
+            reason = None # reason to disable
+            
+            # tagged-values to run
+            if run_tag in test:
+                values = test[run_tag].split()
+                if value not in values:
+                    reason = '%s %s not in run values %s' % (tag, value, values)
+
+            # tagged-values to skip
+            if skip_tag in test:
+                values = test[skip_tag].split()
+                if value in values:
+                    reason = '%s %s in skipped values %s' % (tag, value, values)
+
+            # mark test as disabled if there's a reason
+            if reason:
+                test.setdefault('disabled', reason)        
+
+    def active_tests(self, exists=True, disabled=True, **tags):
+        """
+        - exists : return only existing tests
+        - disabled : whether to return disabled tests
+        - tags : keys and values to filter on (e.g. `os = linux mac`)
+        """
+
+        tests = [i.copy() for i in self.tests] # shallow copy
+        
+        # ignore tests that do not exist
+        if exists:
+            tests = [test for test in tests if os.path.exists(test['path'])]
+
+        # filter by tags
+        for tag, value in tags.items():
+            self.filter(tag, value, tests)
+
+        # ignore disabled tests if specified
+        if not disabled:
+            tests = [test for test in tests
+                     if not 'disabled' in test]
+
+        # return active tests
+        return tests
+
+    def test_paths(self):
+        return [test['path'] for test in self.active_tests()]
+
+
+### utility function(s); probably belongs elsewhere
+
+def convert(directories, pattern=None, ignore=(), write=None):
+    """
+    convert directories to a simple manifest
+    """
+
+    retval = []
+    include = []
+    for directory in directories:
+        for dirpath, dirnames, filenames in os.walk(directory):
+
+            # filter out directory names
+            dirnames = [ i for i in dirnames if i not in ignore ]
+            dirnames.sort()
+
+            # reference only the subdirectory
+            _dirpath = dirpath
+            dirpath = dirpath.split(directory, 1)[-1].strip('/')
+
+            if dirpath.split(os.path.sep)[0] in ignore:
+                continue
+
+            # filter by glob
+            if pattern:
+                filenames = [filename for filename in filenames
+                             if fnmatch(filename, pattern)]
+
+            filenames.sort()
+
+            # write a manifest for each directory
+            if write and (dirnames or filenames):
+                manifest = file(os.path.join(_dirpath, write), 'w')
+                for dirname in dirnames:
+                    print >> manifest, '[include:%s]' % os.path.join(dirname, write)
+                for filename in filenames:
+                    print >> manifest, '[%s]' % filename
+                manifest.close()
+
+            # add to the list
+            retval.extend([os.path.join(dirpath, filename)
+                           for filename in filenames])
+
+    if write:
+        return # the manifests have already been written!
+  
+    retval.sort()
+    retval = ['[%s]' % filename for filename in retval]
+    return '\n'.join(retval)
+
+### command line attributes
+
+class ParserError(Exception):
+  """error for exceptions while parsing the command line"""
+
+def parse_args(_args):
+    """
+    parse and return:
+    --keys=value (or --key value)
+    -tags
+    args
+    """
+
+    # return values
+    _dict = {}
+    tags = []
+    args = []
+
+    # parse the arguments
+    key = None
+    for arg in _args:
+        if arg.startswith('---'):
+            raise ParserError("arguments should start with '-' or '--' only")
+        elif arg.startswith('--'):
+            if key:
+                raise ParserError("Key %s still open" % key)
+            key = arg[2:]
+            if '=' in key:
+                key, value = key.split('=', 1)
+                _dict[key] = value
+                key = None
+                continue
+        elif arg.startswith('-'):
+            if key:
+                raise ParserError("Key %s still open" % key)
+            tags.append(arg[1:])
+            continue
+        else:
+            if key:
+                _dict[key] = arg
+                continue
+            args.append(arg)
+
+    # return values
+    return (_dict, tags, args)
+
+
+### classes for subcommands
+
+class CLICommand(object):
+    usage = '%prog [options] command'
+    def __init__(self, parser):
+      self._parser = parser # master parser
+    def parser(self):
+      return OptionParser(usage=self.usage, description=self.__doc__,
+                          add_help_option=False)
+
+class Copy(CLICommand):
+    usage = '%prog [options] copy manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...'
+    def __call__(self, options, args):
+      # parse the arguments
+      try:
+        kwargs, tags, args = parse_args(args)
+      except ParserError, e:
+        self._parser.error(e.message)
+
+      # make sure we have some manifests, otherwise it will
+      # be quite boring
+      if not len(args) == 2:
+        HelpCLI(self._parser)(options, ['copy'])
+        return
+
+      # read the manifests
+      # TODO: should probably ensure these exist here
+      manifests = ManifestParser()
+      manifests.read(args[0])
+
+      # print the resultant query
+      manifests.copy(args[1], None, *tags, **kwargs)
+
+
+class CreateCLI(CLICommand):
+    """
+    create a manifest from a list of directories
+    """
+    usage = '%prog [options] create directory <directory> <...>'
+
+    def parser(self):
+        parser = CLICommand.parser(self)
+        parser.add_option('-p', '--pattern', dest='pattern',
+                          help="glob pattern for files")
+        parser.add_option('-i', '--ignore', dest='ignore',
+                          default=[], action='append',
+                          help='directories to ignore')
+        parser.add_option('-w', '--in-place', dest='in_place',
+                          help='Write .ini files in place; filename to write to')
+        return parser
+
+    def __call__(self, _options, args):
+        parser = self.parser()
+        options, args = parser.parse_args(args)
+
+        # need some directories
+        if not len(args):
+            parser.print_usage()
+            return
+
+        # add the directories to the manifest
+        for arg in args:
+            assert os.path.exists(arg)
+            assert os.path.isdir(arg)
+            manifest = convert(args, pattern=options.pattern, ignore=options.ignore,
+                               write=options.in_place)
+        if manifest:
+            print manifest
+
+
+class WriteCLI(CLICommand):
+    """
+    write a manifest based on a query
+    """
+    usage = '%prog [options] write manifest <manifest> -tag1 -tag2 --key1=value1 --key2=value2 ...'
+    def __call__(self, options, args):
+
+        # parse the arguments
+        try:
+            kwargs, tags, args = parse_args(args)
+        except ParserError, e:
+            self._parser.error(e.message)
+
+        # make sure we have some manifests, otherwise it will
+        # be quite boring
+        if not args:
+            HelpCLI(self._parser)(options, ['write'])
+            return
+
+        # read the manifests
+        # TODO: should probably ensure these exist here
+        manifests = ManifestParser()
+        manifests.read(*args)
+
+        # print the resultant query
+        manifests.write(global_tags=tags, global_kwargs=kwargs)
+      
+
+class HelpCLI(CLICommand):
+    """
+    get help on a command
+    """
+    usage = '%prog [options] help [command]'
+
+    def __call__(self, options, args):
+        if len(args) == 1 and args[0] in commands:
+            commands[args[0]](self._parser).parser().print_help()
+        else:
+            self._parser.print_help()
+            print '\nCommands:'
+            for command in sorted(commands):
+                print '  %s : %s' % (command, commands[command].__doc__.strip())
+
+class SetupCLI(CLICommand):
+    """
+    setup using setuptools
+    """
+    # use setup.py from the repo when you want to distribute to python!
+    # otherwise setuptools will complain that it can't find setup.py
+    # and result in a useless package
+    
+    usage = '%prog [options] setup [setuptools options]'
+    
+    def __call__(self, options, args):
+        sys.argv = [sys.argv[0]] + args
+        assert setup is not None, "You must have setuptools installed to use SetupCLI"
+        here = os.path.dirname(os.path.abspath(__file__))
+        try:
+            filename = os.path.join(here, 'README.txt')
+            description = file(filename).read()
+        except:    
+            description = ''
+        os.chdir(here)
+
+        setup(name='ManifestDestiny',
+              version=version,
+              description="universal reader for manifests",
+              long_description=description,
+              classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+              keywords='mozilla manifests',
+              author='Jeff Hammel',
+              author_email='jhammel@mozilla.com',
+              url='https://wiki.mozilla.org/Auto-tools/Projects/ManifestDestiny',
+              license='MPL',
+              zip_safe=False,
+              py_modules=['manifestparser'],
+              install_requires=[
+                  # -*- Extra requirements: -*-
+                  ],
+              entry_points="""
+              [console_scripts]
+              manifestparser = manifestparser:main
+              """,
+              )
+
+
+class UpdateCLI(CLICommand):
+    """
+    update the tests as listed in a manifest from a directory
+    """
+    usage = '%prog [options] update manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...'
+
+    def __call__(self, options, args):
+        # parse the arguments
+        try:
+            kwargs, tags, args = parse_args(args)
+        except ParserError, e:
+            self._parser.error(e.message)
+
+        # make sure we have some manifests, otherwise it will
+        # be quite boring
+        if not len(args) == 2:
+            HelpCLI(self._parser)(options, ['update'])
+            return
+
+        # read the manifests
+        # TODO: should probably ensure these exist here
+        manifests = ManifestParser()
+        manifests.read(args[0])
+
+        # print the resultant query
+        manifests.update(args[1], None, *tags, **kwargs)
+
+
+# command -> class mapping
+commands = { 'create': CreateCLI,
+             'help': HelpCLI,
+             'update': UpdateCLI,
+             'write': WriteCLI }
+if setup is not None:
+    commands['setup'] = SetupCLI
+
+def main(args=sys.argv[1:]):
+    """console_script entry point"""
+
+    # set up an option parser
+    usage = '%prog [options] [command] ...'
+    description = __doc__
+    parser = OptionParser(usage=usage, description=description)
+    parser.add_option('-s', '--strict', dest='strict',
+                      action='store_true', default=False,
+                      help='adhere strictly to errors')
+    parser.disable_interspersed_args()
+
+    options, args = parser.parse_args(args)
+
+    if not args:
+        HelpCLI(parser)(options, args)
+        parser.exit()
+
+    # get the command
+    command = args[0]
+    if command not in commands:
+        parser.error("Command must be one of %s (you gave '%s')" % (', '.join(sorted(commands.keys())), command))
+
+    handler = commands[command](parser)
+    handler(options, args[1:])
+
+if __name__ == '__main__':
+    main()
--- a/config/rules.mk
+++ b/config/rules.mk
@@ -137,19 +137,20 @@ define _INSTALL_TESTS
 $(TEST_INSTALLER) $(wildcard $(srcdir)/$(dir)/*) $(testxpcobjdir)/$(relativesrcdir)/$(dir)
 
 endef # do not remove the blank line!
 
 SOLO_FILE ?= $(error Specify a test filename in SOLO_FILE when using check-interactive or check-one)
 
 libs::
 	$(foreach dir,$(XPCSHELL_TESTS),$(_INSTALL_TESTS))
-	$(PYTHON) $(MOZILLA_DIR)/config/buildlist.py \
-	  $(testxpcobjdir)/all-test-dirs.list \
-	  $(addprefix $(relativesrcdir)/,$(XPCSHELL_TESTS))
+	$(PYTHON) $(MOZILLA_DIR)/build/xpccheck.py \
+	  $(topsrcdir) \
+	  $(topsrcdir)/testing/xpcshell/xpcshell.ini \
+	  $(addprefix $(MOZILLA_DIR)/$(relativesrcdir)/,$(XPCSHELL_TESTS))
 
 testxpcsrcdir = $(topsrcdir)/testing/xpcshell
 
 # Execute all tests in the $(XPCSHELL_TESTS) directories.
 # See also testsuite-targets.mk 'xpcshell-tests' target for global execution.
 xpcshell-tests:
 	$(PYTHON) -u $(topsrcdir)/config/pythonpath.py \
 	  -I$(topsrcdir)/build \
--- a/js/src/config/rules.mk
+++ b/js/src/config/rules.mk
@@ -137,19 +137,20 @@ define _INSTALL_TESTS
 $(TEST_INSTALLER) $(wildcard $(srcdir)/$(dir)/*) $(testxpcobjdir)/$(relativesrcdir)/$(dir)
 
 endef # do not remove the blank line!
 
 SOLO_FILE ?= $(error Specify a test filename in SOLO_FILE when using check-interactive or check-one)
 
 libs::
 	$(foreach dir,$(XPCSHELL_TESTS),$(_INSTALL_TESTS))
-	$(PYTHON) $(MOZILLA_DIR)/config/buildlist.py \
-	  $(testxpcobjdir)/all-test-dirs.list \
-	  $(addprefix $(relativesrcdir)/,$(XPCSHELL_TESTS))
+	$(PYTHON) $(MOZILLA_DIR)/build/xpccheck.py \
+	  $(topsrcdir) \
+	  $(topsrcdir)/testing/xpcshell/xpcshell.ini \
+	  $(addprefix $(MOZILLA_DIR)/$(relativesrcdir)/,$(XPCSHELL_TESTS))
 
 testxpcsrcdir = $(topsrcdir)/testing/xpcshell
 
 # Execute all tests in the $(XPCSHELL_TESTS) directories.
 # See also testsuite-targets.mk 'xpcshell-tests' target for global execution.
 xpcshell-tests:
 	$(PYTHON) -u $(topsrcdir)/config/pythonpath.py \
 	  -I$(topsrcdir)/build \
--- a/services/crypto/component/tests/Makefile.in
+++ b/services/crypto/component/tests/Makefile.in
@@ -35,16 +35,16 @@
 # the terms of any one of the MPL, the GPL or the LGPL.
 #
 # ***** END LICENSE BLOCK *****
 
 DEPTH     = ../../../..
 topsrcdir = @top_srcdir@
 srcdir    = @srcdir@
 VPATH     = @srcdir@
-relativesrcdir = services/crypto/components/tests
+relativesrcdir = services/crypto/component/tests
 
 include $(DEPTH)/config/autoconf.mk
 
 MODULE = test_services_crypto
 XPCSHELL_TESTS = unit
 
 include $(topsrcdir)/config/rules.mk
--- a/testing/testsuite-targets.mk
+++ b/testing/testsuite-targets.mk
@@ -160,17 +160,17 @@ GARBAGE += $(addsuffix .log,$(MOCHITESTS
 
 # Execute all xpcshell tests in the directories listed in the manifest.
 # See also config/rules.mk 'xpcshell-tests' target for local execution.
 # Usage: |make [TEST_PATH=...] [EXTRA_TEST_ARGS=...] xpcshell-tests|.
 xpcshell-tests:
 	$(PYTHON) -u $(topsrcdir)/config/pythonpath.py \
 	  -I$(topsrcdir)/build \
 	  $(topsrcdir)/testing/xpcshell/runxpcshelltests.py \
-	  --manifest=$(DEPTH)/_tests/xpcshell/all-test-dirs.list \
+	  --manifest=$(DEPTH)/_tests/xpcshell/xpcshell.ini \
 	  --no-logfiles \
           $(SYMBOLS_PATH) \
 	  $(TEST_PATH_ARG) $(EXTRA_TEST_ARGS) \
 	  $(LIBXUL_DIST)/bin/xpcshell
 
 # install and run the mozmill tests
 $(DEPTH)/_tests/mozmill:
 	$(MAKE) -C $(DEPTH)/testing/mozmill install-develop PKG_STAGE=../../_tests
--- a/testing/xpcshell/Makefile.in
+++ b/testing/xpcshell/Makefile.in
@@ -58,16 +58,17 @@ TEST_HARNESS_FILES := \
   runxpcshelltests.py \
   remotexpcshelltests.py \
   head.js \
   $(NULL)
 
 # Extra files needed from $(topsrcdir)/build
 EXTRA_BUILD_FILES := \
   automationutils.py \
+  manifestparser.py \
   poster.zip \
   $(NULL)
 
 # And files for running xpcshell remotely from $(topsrcdir)/build/mobile
 MOBILE_BUILD_FILES := \
   devicemanager.py \
   $(NULL)
 
@@ -76,15 +77,19 @@ MOBILE_BUILD_FILES := \
 TEST_HARNESS_COMPONENTS := \
   httpd.js \
   httpd.manifest \
   $(NULL)
 
 # Rules for staging the necessary harness bits for a test package
 PKG_STAGE = $(DIST)/test-package-stage
 
+libs::
+	$(INSTALL) $(srcdir)/xpcshell.ini $(DEPTH)/_tests/xpcshell
+	cp $(srcdir)/xpcshell.ini $(DEPTH)/_tests/xpcshell/all-test-dirs.list
+
 stage-package:
 	$(NSINSTALL) -D $(PKG_STAGE)/xpcshell/tests
 	@(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(TEST_HARNESS_FILES)) | (cd $(PKG_STAGE)/xpcshell && tar -xf -)
 	@(cd $(topsrcdir)/build && tar $(TAR_CREATE_FLAGS) - $(EXTRA_BUILD_FILES)) | (cd $(PKG_STAGE)/xpcshell && tar -xf -)
 	@(cd $(topsrcdir)/build/mobile && tar $(TAR_CREATE_FLAGS) - $(MOBILE_BUILD_FILES)) | (cd $(PKG_STAGE)/xpcshell && tar -xf -)
 	@(cd $(DEPTH)/_tests/xpcshell/ && tar $(TAR_CREATE_FLAGS) - *) | (cd $(PKG_STAGE)/xpcshell/tests && tar -xf -)
 	@(cd $(DIST)/bin/components && tar $(TAR_CREATE_FLAGS) - $(TEST_HARNESS_COMPONENTS)) | (cd $(PKG_STAGE)/bin/components && tar -xf -)
--- a/testing/xpcshell/runxpcshelltests.py
+++ b/testing/xpcshell/runxpcshelltests.py
@@ -38,16 +38,17 @@
 #
 # ***** END LICENSE BLOCK ***** */
 
 import re, sys, os, os.path, logging, shutil, signal, math
 from glob import glob
 from optparse import OptionParser
 from subprocess import Popen, PIPE, STDOUT
 from tempfile import mkdtemp, gettempdir
+import manifestparser
 
 from automationutils import *
 
 """ Control-C handling """
 gotSIGINT = False
 def markGotSIGINT(signum, stackFrame):
   global gotSIGINT 
   gotSIGINT = True
@@ -58,48 +59,33 @@ class XPCShellTests(object):
   oldcwd = os.getcwd()
 
   def __init__(self):
     """ Init logging """
     handler = logging.StreamHandler(sys.stdout)
     self.log.setLevel(logging.INFO)
     self.log.addHandler(handler)
 
-  def readManifest(self):
-    """
-      For a given manifest file, read the contents and populate self.testdirs
-    """
-    manifestdir = os.path.dirname(self.manifest)
-    try:
-      f = open(self.manifest, "r")
-      for line in f:
-        path = os.path.join(manifestdir, line.rstrip())
-        if os.path.isdir(path):
-          self.testdirs.append(path)
-      f.close()
-    except:
-      pass # just eat exceptions
-
   def buildTestList(self):
     """
-      Builds a dict of {"testdir" : ["testfile1", "testfile2", ...], "testdir2"...}.
-      If manifest is given override testdirs to build initial list of directories and tests.
-      If testpath is given, use that, otherwise chunk if requested.
-      The resulting set of tests end up in self.alltests
+      read the xpcshell.ini manifest and set self.alltests to be 
+      an array of test objects.
+
+      if we are chunking tests, it will be done here as well
     """
+    mp = manifestparser.ManifestParser(strict=False)
+    if self.manifest is None:
+      for testdir in self.testdirs:
+        if testdir:
+          mp.read(os.path.join(testdir, 'xpcshell.ini'))
+    else:
+      mp.read(self.manifest)
     self.buildTestPath()
 
-    self.alltests = {}
-    if self.manifest is not None:
-      self.readManifest()
-
-    for dir in self.testdirs:
-      tests = self.getTestFiles(dir)
-      if tests:
-        self.alltests[os.path.abspath(dir)] = tests
+    self.alltests = mp.tests
 
     if self.singleFile is None and self.totalChunks > 1:
       self.chunkTests()
 
   def chunkTests(self):
     """
       Split the list of tests up into [totalChunks] pieces and filter the
       self.alltests based on thisChunk, so we only run a subset.
@@ -240,60 +226,45 @@ class XPCShellTests(object):
     """
     self.singleFile = None
     if self.testPath is not None:
       if self.testPath.endswith('.js'):
         # Split into path and file.
         if self.testPath.find('/') == -1:
           # Test only.
           self.singleFile = self.testPath
-          self.testPath = None
         else:
           # Both path and test.
           # Reuse |testPath| temporarily.
           self.testPath = self.testPath.rsplit('/', 1)
           self.singleFile = self.testPath[1]
           self.testPath = self.testPath[0]
       else:
         # Path only.
         # Simply remove optional ending separator.
         self.testPath = self.testPath.rstrip("/")
 
-  def getHeadFiles(self, testdir):
-    """
-      Get the list of head files for a given test directory.
-      On a remote system, this is overloaded to list files in a remote directory structure.
+
+  def getHeadFiles(self, test):
     """
-    return [f for f in sorted(glob(os.path.join(testdir, "head_*.js"))) if os.path.isfile(f)]
-
-  def getTailFiles(self, testdir):
-    """
-      Get the list of tail files for a given test directory.
-      Tails are executed in the reverse order, to "match" heads order,
-      as in "h1-h2-h3 then t3-t2-t1".
+      test['head'] is a whitespace delimited list of head files.
+      return the list of head files as paths including the subdir if the head file exists
 
       On a remote system, this is overloaded to list files in a remote directory structure.
     """
-    return [f for f in reversed(sorted(glob(os.path.join(testdir, "tail_*.js")))) if os.path.isfile(f)]
+    return [os.path.join(test['here'], f).strip() for f in sorted(test['head'].split(' ')) if os.path.isfile(os.path.join(test['here'], f))]
 
-  def getTestFiles(self, testdir):
-    """
-      Ff a single test file was specified, we only want to execute that test,
-      otherwise return a list of all tests in a directory
-
-      On a remote system, this is overloaded to find files in the remote directory structure.
+  def getTailFiles(self, test):
     """
-    testfiles = sorted(glob(os.path.join(os.path.abspath(testdir), "test_*.js")))
-    if self.singleFile:
-      if self.singleFile in [os.path.basename(x) for x in testfiles]:
-        testfiles = [os.path.abspath(os.path.join(testdir, self.singleFile))]
-      else: # not in this dir? skip it
-        return None
-            
-    return testfiles
+      test['tail'] is a whitespace delimited list of head files.
+      return the list of tail files as paths including the subdir if the tail file exists
+
+      On a remote system, this is overloaded to list files in a remote directory structure.
+    """
+    return [os.path.join(test['here'], f).strip() for f in sorted(test['tail'].split(' ')) if os.path.isfile(os.path.join(test['here'], f))]
 
   def setupProfileDir(self):
     """
       Create a temporary folder for the profile and set appropriate environment variables.
       When running check-interactive and check-one, the directory is well-defined and
       retained for inspection once the tests complete.
 
       On a remote system, we overload this to use a remote path structure.
@@ -457,94 +428,97 @@ class XPCShellTests(object):
 
     self.setAbsPath()
     self.buildXpcsRunArgs()
     self.buildEnvironment()
     pStdout, pStderr = self.getPipes()
 
     self.buildTestList()
 
-    for testdir in sorted(self.alltests.keys()):
-      if self.testPath and not testdir.endswith(self.testPath):
+    for test in self.alltests:
+      name = test['path']
+      if self.singleFile and not name.endswith(self.singleFile):
         continue
 
+      if self.testPath and name.find(self.testPath) == -1:
+        continue
+
+      testdir = os.path.dirname(name)
       self.buildXpcsCmd(testdir)
-      testHeadFiles = self.getHeadFiles(testdir)
-      testTailFiles = self.getTailFiles(testdir)
+      testHeadFiles = self.getHeadFiles(test)
+      testTailFiles = self.getTailFiles(test)
       cmdH = self.buildCmdHead(testHeadFiles, testTailFiles, self.xpcsCmd)
 
-      # Now execute each test individually.
-      for test in self.alltests[testdir]:
-        # create a temp dir that the JS harness can stick a profile in
-        self.profileDir = self.setupProfileDir()
-        self.leakLogFile = self.setupLeakLogging()
+      # create a temp dir that the JS harness can stick a profile in
+      self.profileDir = self.setupProfileDir()
+      self.leakLogFile = self.setupLeakLogging()
 
-        # The test file will have to be loaded after the head files.
-        cmdT = ['-e', 'const _TEST_FILE = ["%s"];' %
-                replaceBackSlashes(test)]
+      # The test file will have to be loaded after the head files.
+      cmdT = ['-e', 'const _TEST_FILE = ["%s"];' %
+                replaceBackSlashes(name)]
 
-        try:
-          print "TEST-INFO | %s | running test ..." % test
+      try:
+        print "TEST-INFO | %s | running test ..." % name
 
-          proc = self.launchProcess(cmdH + cmdT + self.xpcsRunArgs,
-                      stdout=pStdout, stderr=pStderr, env=self.env, cwd=testdir)
+        proc = self.launchProcess(cmdH + cmdT + self.xpcsRunArgs,
+                    stdout=pStdout, stderr=pStderr, env=self.env, cwd=testdir)
 
-          # Allow user to kill hung subprocess with SIGINT w/o killing this script
-          # - don't move this line above launchProcess, or child will inherit the SIG_IGN
-          signal.signal(signal.SIGINT, markGotSIGINT)
-          # |stderr == None| as |pStderr| was either |None| or redirected to |stdout|.
-          stdout, stderr = self.communicate(proc)
-          signal.signal(signal.SIGINT, signal.SIG_DFL)
+        # Allow user to kill hung subprocess with SIGINT w/o killing this script
+        # - don't move this line above launchProcess, or child will inherit the SIG_IGN
+        signal.signal(signal.SIGINT, markGotSIGINT)
+        # |stderr == None| as |pStderr| was either |None| or redirected to |stdout|.
+        stdout, stderr = self.communicate(proc)
+        signal.signal(signal.SIGINT, signal.SIG_DFL)
 
-          if interactive:
-            # Not sure what else to do here...
-            return True
+        if interactive:
+          # Not sure what else to do here...
+          return True
 
-          def print_stdout(stdout):
-            """Print stdout line-by-line to avoid overflowing buffers."""
-            print ">>>>>>>"
-            for line in stdout.splitlines():
-              print line
-            print "<<<<<<<"
+        def print_stdout(stdout):
+          """Print stdout line-by-line to avoid overflowing buffers."""
+          print ">>>>>>>"
+          for line in stdout.splitlines():
+            print line
+          print "<<<<<<<"
 
-          if (self.getReturnCode(proc) != 0) or \
-              (stdout and re.search("^((parent|child): )?TEST-UNEXPECTED-", stdout, re.MULTILINE)) or \
-              (stdout and re.search(": SyntaxError:", stdout, re.MULTILINE)):
-            print "TEST-UNEXPECTED-FAIL | %s | test failed (with xpcshell return code: %d), see following log:" % (test, self.getReturnCode(proc))
+        if (self.getReturnCode(proc) != 0) or \
+            (stdout and re.search("^((parent|child): )?TEST-UNEXPECTED-", stdout, re.MULTILINE)) or \
+            (stdout and re.search(": SyntaxError:", stdout, re.MULTILINE)):
+          print "TEST-UNEXPECTED-FAIL | %s | test failed (with xpcshell return code: %d), see following log:" % (name, self.getReturnCode(proc))
+          print_stdout(stdout)
+          failCount += 1
+        else:
+          print "TEST-PASS | %s | test passed" % name
+          if verbose:
             print_stdout(stdout)
-            failCount += 1
-          else:
-            print "TEST-PASS | %s | test passed" % test
-            if verbose:
-              print_stdout(stdout)
-            passCount += 1
+          passCount += 1
 
-          checkForCrashes(testdir, self.symbolsPath, testName=test)
-          # Find child process(es) leak log(s), if any: See InitLog() in
-          # xpcom/base/nsTraceRefcntImpl.cpp for logfile naming logic
-          leakLogs = [self.leakLogFile]
-          for childLog in glob(os.path.join(self.profileDir, "runxpcshelltests_leaks_*_pid*.log")):
-            if os.path.isfile(childLog):
-              leakLogs += [childLog]
-          for log in leakLogs:
-            dumpLeakLog(log, True)
+        checkForCrashes(testdir, self.symbolsPath, testName=name)
+        # Find child process(es) leak log(s), if any: See InitLog() in
+        # xpcom/base/nsTraceRefcntImpl.cpp for logfile naming logic
+        leakLogs = [self.leakLogFile]
+        for childLog in glob(os.path.join(self.profileDir, "runxpcshelltests_leaks_*_pid*.log")):
+          if os.path.isfile(childLog):
+            leakLogs += [childLog]
+        for log in leakLogs:
+          dumpLeakLog(log, True)
 
-          if self.logfiles and stdout:
-            self.createLogFile(test, stdout, leakLogs)
-        finally:
-          # We don't want to delete the profile when running check-interactive
-          # or check-one.
-          if self.profileDir and not self.interactive and not self.singleFile:
-            self.removeDir(self.profileDir)
-        if gotSIGINT:
-          print "TEST-UNEXPECTED-FAIL | Received SIGINT (control-C) during test execution"
-          if (keepGoing):
-            gotSIGINT = False
-          else:
-            break
+        if self.logfiles and stdout:
+          self.createLogFile(name, stdout, leakLogs)
+      finally:
+        # We don't want to delete the profile when running check-interactive
+        # or check-one.
+        if self.profileDir and not self.interactive and not self.singleFile:
+          self.removeDir(self.profileDir)
+      if gotSIGINT:
+        print "TEST-UNEXPECTED-FAIL | Received SIGINT (control-C) during test execution"
+        if (keepGoing):
+          gotSIGINT = False
+        else:
+          break
     if passCount == 0 and failCount == 0:
       print "TEST-UNEXPECTED-FAIL | runxpcshelltests.py | No tests run. Did you pass an invalid --test-path?"
       failCount = 1
 
     print """INFO | Result summary:
 INFO | Passed: %d
 INFO | Failed: %d""" % (passCount, failCount)
 
--- a/toolkit/mozapps/extensions/test/Makefile.in
+++ b/toolkit/mozapps/extensions/test/Makefile.in
@@ -70,9 +70,10 @@ libs::
 	if [ -d $(ADDONSRC) ]; then \
 		$(EXIT_ON_ERROR) \
 		for dir in $(ADDONSRC)/*; do \
 			base=`basename $$dir` ; \
 			(cd $$dir && zip -r $(TESTXPI)/$$base.xpi *) \
 		done \
 	fi
 	cd $(TESTROOT)/xpcshell/ && tar -cPf - . | (cd $(TESTROOT)/xpcshell-unpack && tar -xPvf - )
-
+	sed s/head_addons.js/head_addons.js\ head_unpack.js/ $(TESTROOT)/xpcshell-unpack/xpcshell.ini > $(TESTROOT)/xpcshell-unpack/xpcshell.in_
+	mv $(TESTROOT)/xpcshell-unpack/xpcshell.in_ $(TESTROOT)/xpcshell-unpack/xpcshell.ini