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 69772 40195c0187d3069b848b012065476ab8868e6d0c
parent 69771 a365ca6c2379320357bf8ffa54006a916d02ed7d
child 69773 9e2f8321f4800c3ff03f26bda29930afba94975a
push idunknown
push userunknown
push dateunknown
reviewersted, test-only
bugs616999
milestone6.0a1
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