Example #1
0
def _do_test_raw(script,
                 path='foo.js',
                 bootstrap=False,
                 ignore_pollution=True,
                 detected_type=None,
                 jetpack=False,
                 instant=True):
    """Perform a test on a JS file."""

    err = ErrorBundle(instant=instant)
    if jetpack:
        err.metadata['is_jetpack'] = True

    err.handler = OutputHandler(sys.stdout, True)
    err.supported_versions = {}
    if bootstrap:
        err.save_resource('em:bootstrap', True)
    if detected_type:
        err.detected_type = detected_type

    validator.testcases.content._process_file(err, MockXPI(), path, script,
                                              path.lower(),
                                              not ignore_pollution)
    if err.final_context is not None:
        print 'CONTEXT', repr(err.final_context.keys())

    return err
Example #2
0
    def print_summary(self, verbose=False, no_color=False):
        "Prints a summary of the validation process so far."

        types = {
            0: "Unknown",
            1: "Extension/Multi-Extension",
            2: "Full Theme",
            3: "Dictionary",
            4: "Language Pack",
            5: "Search Provider",
            7: "Subpackage",
            8: "App",
        }
        detected_type = types[self.detected_type]

        buffer = StringIO()
        self.handler = OutputHandler(buffer, no_color)

        # Make a neat little printout.
        self.handler.write("\n<<GREEN>>Summary:").write("-" * 30).write(
            "Detected type: <<BLUE>>%s" % detected_type
        ).write("-" * 30)

        if self.failed():
            self.handler.write("<<BLUE>>Test failed! Errors:")

            # Print out all the errors/warnings:
            for error in self.errors:
                self._print_message("<<RED>>Error:<<NORMAL>>\t", error, verbose)
            for warning in self.warnings:
                self._print_message("<<YELLOW>>Warning:<<NORMAL>> ", warning, verbose)
        else:
            self.handler.write("<<GREEN>>All tests succeeded!")

        if self.notices:
            for notice in self.notices:
                self._print_message(prefix="<<WHITE>>Notice:<<NORMAL>>\t", message=notice, verbose=verbose)

        if "is_jetpack" in self.metadata and verbose:
            self.handler.write("\n")
            self.handler.write("<<GREEN>>Jetpack add-on detected.<<NORMAL>>\n" "Identified files:")
            if "jetpack_identified_files" in self.metadata:
                for filename, data in self.metadata["jetpack_identified_files"].items():
                    self.handler.write((" %s\n" % filename) + ("  %s : %s" % data))

            if "jetpack_unknown_files" in self.metadata:
                self.handler.write("Unknown files:")
                for filename in self.metadata["jetpack_unknown_files"]:
                    self.handler.write(" %s" % filename)

        self.handler.write("\n")
        if self.unfinished:
            self.handler.write("<<RED>>Validation terminated early")
            self.handler.write("Errors during validation are preventing " "the validation proecss from completing.")
            self.handler.write("Use the <<YELLOW>>--determined<<NORMAL>> " "flag to ignore these errors.")
            self.handler.write("\n")

        return buffer.getvalue()
Example #3
0
def _do_test(path):
    script = validator.unicodehelper.decode(open(path, 'rb').read())
    print script.encode('ascii', 'replace')

    err = ErrorBundle(instant=True)
    err.supported_versions = {}
    err.handler = OutputHandler(sys.stdout, False)
    validator.testcases.scripting.test_js_file(err, path, script)
    return err
Example #4
0
    def setup_err(self, for_appversions=None):
        """
        Instantiate the error bundle object. Use the `instant` parameter to
        have it output errors as they're generated. `for_appversions` may be set
        to target the test cases at a specific Gecko version range.

        An existing error bundle will be overwritten with a fresh one that has
        the state that the test case was setup with.
        """
        self.err = ErrorBundle(instant=True,
                               for_appversions=for_appversions or {},
                               listed=self.listed)
        self.err.handler = OutputHandler(sys.stdout, True)

        if self.is_jetpack:
            self.err.metadata['is_jetpack'] = True
        if self.is_bootstrapped:
            self.err.save_resource('em:bootstrap', True)
        if self.detected_type is not None:
            self.err.detected_Type = self.detected_type
Example #5
0
def _do_test_raw(script, path="foo.js", bootstrap=False, ignore_pollution=True,
                 detected_type=None, jetpack=False):
    "Performs a test on a JS file"

    err = ErrorBundle(instant=True)
    err.save_resource("SPIDERMONKEY", False)
    if jetpack:
        err.metadata["is_jetpack"] = True

    err.handler = OutputHandler(sys.stdout, True)
    err.supported_versions = {}
    if bootstrap:
        err.save_resource("em:bootstrap", True)
    if detected_type:
        err.detected_type = detected_type

    validator.testcases.content._process_file(
            err, MockXPI(), path, script, path.lower(), not ignore_pollution)
    if err.final_context is not None:
        print err.final_context.output()

    return err
    def print_summary(self, verbose=False, no_color=False):
        'Prints a summary of the validation process so far.'

        types = {0: 'Unknown',
                 1: 'Extension/Multi-Extension',
                 2: 'Full Theme',
                 3: 'Dictionary',
                 4: 'Language Pack',
                 5: 'Search Provider',
                 7: 'Subpackage',
                 8: 'App'}
        detected_type = types[self.detected_type]

        buffer = StringIO()
        self.handler = OutputHandler(buffer, no_color)

        # Make a neat little printout.
        self.handler.write('\n<<GREEN>>Summary:') \
            .write('-' * 30) \
            .write('Detected type: <<BLUE>>%s' % detected_type) \
            .write('-' * 30)

        if self.failed():
            self.handler.write('<<BLUE>>Test failed! Errors:')

            # Print out all the errors/warnings:
            for error in self.errors:
                self._print_message('<<RED>>Error:<<NORMAL>>\t',
                                    error, verbose)
            for warning in self.warnings:
                self._print_message('<<YELLOW>>Warning:<<NORMAL>> ',
                                    warning, verbose)
        else:
            self.handler.write('<<GREEN>>All tests succeeded!')

        if self.notices:
            for notice in self.notices:
                self._print_message(prefix='<<WHITE>>Notice:<<NORMAL>>\t',
                                    message=notice,
                                    verbose=verbose)

        if 'is_jetpack' in self.metadata and verbose:
            self.handler.write('\n')
            self.handler.write('<<GREEN>>Jetpack add-on detected.<<NORMAL>>\n'
                               'Identified files:')
            if 'jetpack_identified_files' in self.metadata:
                for filename, data in \
                        self.metadata['jetpack_identified_files'].items():
                    self.handler.write((' %s\n' % filename) +
                                       ('  %s : %s' % data))

            if 'jetpack_unknown_files' in self.metadata:
                self.handler.write('Unknown files:')
                for filename in self.metadata['jetpack_unknown_files']:
                    self.handler.write(' %s' % filename)

        self.handler.write('\n')
        if self.unfinished:
            self.handler.write('<<RED>>Validation terminated early')
            self.handler.write('Errors during validation are preventing '
                               'the validation process from completing.')
            self.handler.write('Use the <<YELLOW>>--determined<<NORMAL>> '
                               'flag to ignore these errors.')
            self.handler.write('\n')

        return buffer.getvalue()
class ErrorBundle(object):
    """This class does all sorts of cool things. It gets passed around
    from test to test and collects up all the errors like the candy man
    'separating the sorrow and collecting up all the cream.' It's
    borderline magical.

    Keyword Arguments

    **determined**
        Whether the validator should continue after a tier fails
    **listed**
        True if the add-on is destined for AMO, false if not
    **instant**
        Who knows what this does
    **overrides**
        dict of install.rdf values to override. Possible keys:
        targetapp_minVersion, targetapp_maxVersion
    **for_appversions**
        A dict of app GUIDs referencing lists of versions. Determines which
        version-dependant tests should be run.
    """

    def __init__(self, determined=True, listed=True, instant=False,
                 overrides=None, for_appversions=None):

        self.handler = None

        self.errors = []
        self.warnings = []
        self.notices = []
        self.message_tree = {}

        self.compat_summary = {'errors': 0,
                               'warnings': 0,
                               'notices': 0}
        self.signing_summary = {s: 0 for s in SIGNING_SEVERITIES}

        self.ending_tier = 1
        self.tier = 1

        self.subpackages = []
        self.package_stack = []

        self.detected_type = 0
        self.unfinished = False

        # TODO: Break off into resource helper
        self.resources = {}
        self.pushable_resources = {}
        self.final_context = None

        self.metadata = {'requires_chrome': False, 'listed': listed,
                         'validator_version': validator.__version__}
        if listed:
            self.resources['listed'] = True
        self.instant = instant
        self.determined = determined

        self.version_requirements = None

        self.overrides = overrides or None

        self.supported_versions = self.for_appversions = for_appversions

    def _message(type_, message_type):
        def wrap(self, *args, **kwargs):
            message = {
                'id': kwargs.get('err_id') or args[0],
                'message': kwargs.get(message_type) or args[1],
                'description': kwargs.get('description',
                                          args[2] if len(args) > 2 else None),
                # Filename is never None.
                'file': kwargs.get('filename',
                                   args[3] if len(args) > 3 else ''),
                'line': kwargs.get('line',
                                   args[4] if len(args) > 4 else None),
                'column': kwargs.get('column',
                                     args[5] if len(args) > 5 else None),
                # If true, the message should only be shown to editors.
                'editors_only': kwargs.get('editors_only', False),
            }
            for field in ('tier', 'for_appversions', 'compatibility_type', ):
                message[field] = kwargs.get(field)

            if 'signing_severity' in kwargs:
                severity = kwargs['signing_severity']

                assert severity in SIGNING_SEVERITIES

                if not kwargs.get('from_merge'):
                    self.signing_summary[severity] += 1
                message['signing_severity'] = severity
            if 'signing_help' in kwargs:
                message['signing_help'] = kwargs['signing_help']

            self._save_message(getattr(self, type_), type_, message,
                               from_merge=kwargs.get('from_merge'),
                               context=kwargs.get('context'))
            return message

        wrap.__name__ = message_type
        return wrap

    # And then all the real functions. Ahh, how clean!
    error = _message('errors', 'error')
    warning = _message('warnings', 'warning')
    notice = _message('notices', 'notice')

    def system_error(self, msg_id=None, message=None, description=None,
                     validation_timeout=False, exc_info=None, **kw):
        """Add an error message for an unexpected exception in validator
        code, and move it to the front of the error message list. If
        `exc_info` is supplied, the error will be logged.

        If the error is a validation timeout, it is re-raised unless
        `msg_id` is "validation_timeout"."""

        if exc_info:
            if (isinstance(exc_info[1], validator.ValidationTimeout) and
                    msg_id != 'validation_timeout'):
                # These should always propagate to the top-level exception
                # handler, and be reported only once.
                raise exc_info[1]

            log.error('Unexpected error during validation: %s: %s'
                      % (exc_info[0].__name__, exc_info[1]),
                      exc_info=exc_info)

        full_id = ('validator', 'unexpected_exception')
        if msg_id:
            full_id += (msg_id,)

        self.error(full_id,
                   message or 'An unexpected error has occurred.',
                   description or
                   ('Validation was unable to complete successfully due '
                    'to an unexpected error.',
                    'The error has been logged, but please consider '
                    'filing an issue report here: '
                    'https://bit.ly/1POrYYU'),
                   tier=1, **kw)

        # Move the error message to the beginning of the list.
        self.errors.insert(0, self.errors.pop())

    def drop_message(self, message):
        """Drop the given message object from the appropriate message list.

        Returns True if the message was found, otherwise False."""

        for type_ in 'errors', 'warnings', 'notices':
            list_ = getattr(self, type_)
            if message in list_:
                list_.remove(message)
                if 'signing_severity' in message:
                    self.signing_summary[message['signing_severity']] -= 1
                return True

        return False

    def set_tier(self, tier):
        'Updates the tier and ending tier'
        self.tier = tier
        if tier > self.ending_tier:
            self.ending_tier = tier

    @property
    def message_count(self):
        return len(self.errors) + len(self.warnings) + len(self.notices)

    def _save_message(self, stack, type_, message, context=None,
                      from_merge=False):
        'Stores a message in the appropriate message stack.'

        uid = uuid.uuid4().hex
        message['uid'] = uid

        # Get the context for the message (if there's a context available)
        if context is not None:
            if isinstance(context, tuple):
                message['context'] = context
            else:
                message['context'] = (
                    context.get_context(line=message['line'],
                                        column=message['column']))
        else:
            message['context'] = None

        if self.package_stack:
            if not isinstance(message['file'], list):
                message['file'] = [message['file']]

            message['file'] = self.package_stack + message['file']

        # Test that if for_appversions is set that we're only applying to
        # supported add-ons. THIS IS THE LAST FILTER BEFORE THE MESSAGE IS
        # ADDED TO THE STACK!
        if message['for_appversions']:
            if not self.supports_version(message['for_appversions']):
                if self.instant:
                    print '(Instant error discarded)'
                    self._print_message(type_, message, verbose=True)
                return
        elif self.version_requirements:
            # If there was no for_appversions but there were version
            # requirements detailed in the decorator, use the ones from the
            # decorator.
            message['for_appversions'] = self.version_requirements

        # Save the message to the stack.
        stack.append(message)

        # Mark the tier that the error occurred at.
        if message['tier'] is None:
            message['tier'] = self.tier

        # Build out the compatibility summary if possible.
        if message['compatibility_type'] and not from_merge:
            self.compat_summary['%ss' % message['compatibility_type']] += 1

        # Build out the message tree entry.
        if message['id']:
            tree = self.message_tree
            last_id = None
            for eid in message['id']:
                if last_id is not None:
                    tree = tree[last_id]
                if eid not in tree:
                    tree[eid] = {'__errors': 0,
                                 '__warnings': 0,
                                 '__notices': 0,
                                 '__messages': []}
                tree[eid]['__%s' % type_] += 1
                last_id = eid

            tree[last_id]['__messages'].append(uid)

        # If instant mode is turned on, output the message immediately.
        if self.instant:
            self._print_message(type_, message, verbose=True)

    def failed(self, fail_on_warnings=True):
        """Returns a boolean value describing whether the validation
        succeeded or not."""

        return bool(self.errors) or (fail_on_warnings and bool(self.warnings))

    def get_resource(self, name):
        'Retrieves an object that has been stored by another test.'

        if name in self.resources:
            return self.resources[name]
        elif name in self.pushable_resources:
            return self.pushable_resources[name]
        else:
            return False

    def save_resource(self, name, resource, pushable=False):
        'Saves an object such that it can be used by other tests.'

        if pushable:
            self.pushable_resources[name] = resource
        else:
            self.resources[name] = resource

    @property
    def is_nested_package(self):
        'Returns whether the current package is within a PACKAGE_MULTI'
        return bool(self.package_stack)

    def push_state(self, new_file=''):
        'Saves the current error state to parse subpackages'

        self.subpackages.append({'detected_type': self.detected_type,
                                 'message_tree': self.message_tree,
                                 'resources': self.pushable_resources,
                                 'metadata': self.metadata})

        self.message_tree = {}
        self.pushable_resources = {}
        self.metadata = {'requires_chrome': False,
                         'listed': self.metadata.get('listed'),
                         'validator_version': validator.__version__}

        self.package_stack.append(new_file)

    def pop_state(self):
        'Retrieves the last saved state and restores it.'

        # Save a copy of the current state.
        state = self.subpackages.pop()
        metadata = self.metadata
        # We only rebuild message_tree anyway. No need to restore.

        # Copy the existing state back into place
        self.detected_type = state['detected_type']
        self.message_tree = state['message_tree']
        self.pushable_resources = state['resources']
        self.metadata = state['metadata']

        name = self.package_stack.pop()

        self.metadata.setdefault('sub_packages', {})[name] = metadata

    def render_json(self):
        'Returns a JSON summary of the validation operation.'

        types = {0: 'unknown',
                 1: 'extension',
                 2: 'theme',
                 3: 'dictionary',
                 4: 'langpack',
                 5: 'search',
                 8: 'webapp'}
        output = {'detected_type': types[self.detected_type],
                  'ending_tier': self.ending_tier,
                  'success': not self.failed(),
                  'messages': [],
                  'errors': len(self.errors),
                  'warnings': len(self.warnings),
                  'notices': len(self.notices),
                  'message_tree': self.message_tree,
                  'compatibility_summary': self.compat_summary,
                  'signing_summary': self.signing_summary,
                  'metadata': self.metadata}

        messages = output['messages']

        # Copy messages to the JSON output
        for error in self.errors:
            error['type'] = 'error'
            messages.append(error)

        for warning in self.warnings:
            warning['type'] = 'warning'
            messages.append(warning)

        for notice in self.notices:
            notice['type'] = 'notice'
            messages.append(notice)

        # Output the JSON.
        return json.dumps(output)

    def print_summary(self, verbose=False, no_color=False):
        'Prints a summary of the validation process so far.'

        types = {0: 'Unknown',
                 1: 'Extension/Multi-Extension',
                 2: 'Full Theme',
                 3: 'Dictionary',
                 4: 'Language Pack',
                 5: 'Search Provider',
                 7: 'Subpackage',
                 8: 'App'}
        detected_type = types[self.detected_type]

        buffer = StringIO()
        self.handler = OutputHandler(buffer, no_color)

        # Make a neat little printout.
        self.handler.write('\n<<GREEN>>Summary:') \
            .write('-' * 30) \
            .write('Detected type: <<BLUE>>%s' % detected_type) \
            .write('-' * 30)

        if self.failed():
            self.handler.write('<<BLUE>>Test failed! Errors:')

            # Print out all the errors/warnings:
            for error in self.errors:
                self._print_message('<<RED>>Error:<<NORMAL>>\t',
                                    error, verbose)
            for warning in self.warnings:
                self._print_message('<<YELLOW>>Warning:<<NORMAL>> ',
                                    warning, verbose)
        else:
            self.handler.write('<<GREEN>>All tests succeeded!')

        if self.notices:
            for notice in self.notices:
                self._print_message(prefix='<<WHITE>>Notice:<<NORMAL>>\t',
                                    message=notice,
                                    verbose=verbose)

        if 'is_jetpack' in self.metadata and verbose:
            self.handler.write('\n')
            self.handler.write('<<GREEN>>Jetpack add-on detected.<<NORMAL>>\n'
                               'Identified files:')
            if 'jetpack_identified_files' in self.metadata:
                for filename, data in \
                        self.metadata['jetpack_identified_files'].items():
                    self.handler.write((' %s\n' % filename) +
                                       ('  %s : %s' % data))

            if 'jetpack_unknown_files' in self.metadata:
                self.handler.write('Unknown files:')
                for filename in self.metadata['jetpack_unknown_files']:
                    self.handler.write(' %s' % filename)

        self.handler.write('\n')
        if self.unfinished:
            self.handler.write('<<RED>>Validation terminated early')
            self.handler.write('Errors during validation are preventing '
                               'the validation process from completing.')
            self.handler.write('Use the <<YELLOW>>--determined<<NORMAL>> '
                               'flag to ignore these errors.')
            self.handler.write('\n')

        return buffer.getvalue()

    def _flatten_list(self, data):
        'Flattens nested lists into strings.'

        if data is None:
            return ''
        if isinstance(data, types.StringTypes):
            return data
        elif isinstance(data, (list, tuple)):
            return '\n'.join(self._flatten_list(x) for x in data)

    def _print_message(self, prefix, message, verbose=True):
        'Prints a message and takes care of all sorts of nasty code'

        # Load up the standard output.
        output = ['\n', prefix, message['message']]

        # We have some extra stuff for verbose mode.
        if verbose:
            verbose_output = []

            # Detailed problem description.
            if message['description']:
                verbose_output.append(
                    self._flatten_list(message['description']))

            if message.get('signing_severity'):
                verbose_output.append(
                    ('\tAutomated signing severity: %s' %
                     message['signing_severity']))

            if message.get('signing_help'):
                verbose_output.append(
                    '\tSuggestions for passing automated signing:')
                verbose_output.append(
                    self._flatten_list(message['signing_help']))

            # Show the user what tier we're on
            verbose_output.append('\tTier:\t%d' % message['tier'])

            # If file information is available, output that as well.
            files = message['file']
            if files is not None and files != '':
                fmsg = '\tFile:\t%s'

                # Nested files (subpackes) are stored in a list.
                if type(files) is list:
                    if files[-1] == '':
                        files[-1] = '(none)'
                    verbose_output.append(fmsg % ' > '.join(files))
                else:
                    verbose_output.append(fmsg % files)

            # If there is a line number, that gets put on the end.
            if message['line']:
                verbose_output.append('\tLine:\t%s' % message['line'])
            if message['column'] and message['column'] != 0:
                verbose_output.append('\tColumn:\t%d' % message['column'])

            if message.get('context'):
                verbose_output.append('\tContext:')
                verbose_output.extend([('\t> %s' % x
                                        if x is not None
                                        else '\t>' + ('-' * 20))
                                       for x
                                       in message['context']])

            # Stick it in with the standard items.
            output.append('\n')
            output.append('\n'.join(verbose_output))

        # Send the final output to the handler to be rendered.
        self.handler.write(u''.join(map(unicodehelper.decode, output)))

    def supports_version(self, guid_set):
        """
        Returns whether a GUID set in for_appversions format is compatbile with
        the current supported applications list.
        """

        # Don't let the test run if we haven't parsed install.rdf yet.
        if self.supported_versions is None:
            raise Exception('Early compatibility test run before install.rdf '
                            'was parsed.')

        return self._compare_version(requirements=guid_set,
                                     support=self.supported_versions)

    def _compare_version(self, requirements, support):
        """
        Return whether there is an intersection between a support applications
        GUID set and a set of supported applications.
        """

        for guid in requirements:
            # If we support any of the GUIDs in the guid_set, test if any of
            # the provided versions for the GUID are supported.
            if (guid in support and
                any((detected_version in requirements[guid]) for
                    detected_version in support[guid])):
                return True

    def discard_unused_messages(self, ending_tier):
        """
        Delete messages from errors, warnings, and notices whose tier is
        greater than the ending tier.
        """

        stacks = [self.errors, self.warnings, self.notices]
        for stack in stacks:
            for message in stack:
                if message['tier'] > ending_tier:
                    stack.remove(message)
Example #8
0
class ErrorBundle(object):
    """This class does all sorts of cool things. It gets passed around
    from test to test and collects up all the errors like the candy man
    'separating the sorrow and collecting up all the cream.' It's
    borderline magical.

    Keyword Arguments

    **determined**
        Whether the validator should continue after a tier fails
    **listed**
        True if the add-on is destined for AMO, false if not
    **instant**
        Who knows what this does
    **overrides**
        dict of install.rdf values to override. Possible keys:
        targetapp_minVersion, targetapp_maxVersion
    **for_appversions**
        A dict of app GUIDs referencing lists of versions. Determines which
        version-dependant tests should be run.
    """

    def __init__(self, determined=True, listed=True, instant=False,
                 overrides=None, for_appversions=None, debug=0):

        self.handler = None

        self.debug_level = debug
        if debug == 0 and constants.IN_TESTS:
            self.debug_level = 1

        self.errors = []
        self.warnings = []
        self.notices = []
        self.message_tree = {}

        self.compat_summary = {'errors': 0,
                               'warnings': 0,
                               'notices': 0}
        self.signing_summary = {s: 0 for s in SIGNING_SEVERITIES}

        self.ending_tier = 1
        self.tier = 1

        self.subpackages = []
        self.package_stack = []

        self.detected_type = 0
        self.unfinished = False

        # TODO: Break off into resource helper
        self.resources = {}
        self.pushable_resources = {}
        self.final_context = None

        self.metadata = {'requires_chrome': False, 'listed': listed,
                         'validator_version': validator.__version__}
        if listed:
            self.resources['listed'] = True
        self.instant = instant
        self.determined = determined

        self.version_requirements = None

        self.overrides = overrides or None

        self.supported_versions = self.for_appversions = for_appversions

    def _message(type_, message_type):
        def wrap(self, *args, **kwargs):
            message = {
                'description': (),
                'file': '',
                'editors_only': False,
            }

            if 'location' in kwargs:
                loc = kwargs['location']
                message.update({'file': loc.file,
                                'line': loc.line,
                                'column': loc.column})

            # Has to go.
            if 'err_id' in kwargs:
                kwargs['id'] = kwargs['err_id']
            if 'filename' in kwargs:
                kwargs['file'] = kwargs['filename']
            if message_type in kwargs:
                kwargs['message'] = kwargs[message_type]

            positional_args = ('id',
                               'message',
                               'description',
                               'file',
                               'line',
                               'column')

            keys = positional_args + (
                'tier',
                'for_appversions',
                'compatibility_type',
                'editors_only',
                'context_data',
            )

            # This is absurd.
            # Copy positional args into the kwargs dict, if they're missing.
            for key, arg in zip(positional_args, args):
                assert key not in kwargs
                kwargs[key] = arg

            for key in keys:
                message.setdefault(key, None)
                if key in kwargs:
                    message[key] = kwargs[key]

            if 'signing_severity' in kwargs:
                severity = kwargs['signing_severity']
                assert severity in SIGNING_SEVERITIES

                self.signing_summary[severity] += 1
                message['signing_severity'] = severity

            if 'signing_help' in kwargs:
                message['signing_help'] = kwargs['signing_help']

            self._save_message(getattr(self, type_), type_, message,
                               context=kwargs.get('context'))
            return message

        wrap.__name__ = message_type
        return wrap

    # And then all the real functions. Ahh, how clean!
    error = _message('errors', 'error')
    warning = _message('warnings', 'warning')
    notice = _message('notices', 'notice')

    def report(self, base_message, *messages):
        """Create a message from the given base message, updating it with
        properties from each message in a non-keyword argument, and report
        it to the correct reporting function.

        The correct reporting function is determined by the presence of either
        a "error", "warning", or "notice" key in the test properties, as
        expected by the so-named error bundle method.

        Example:

            `report({'err_id': ('javascript', 'dangerous_global', 'generic'),
                     'warning': 'Access to dangerous global',
                     'description': 'Evil. *hiss*'},
                    {'err_id': 'eval',
                     'warning': 'Do not use eval. Ugh.'})`

        Reports a new warning:

            `warning(err_id=('javascript', 'dangerous_global', 'eval'),
                     warning='Do not use eval. Ugh.',
                     description='Evil. *hiss*')`
        """

        # Merge additional message properties into the base message.
        message = reduce(merge_description, messages, base_message)

        # Get the message type based on which message key is included in the
        # properties.
        TYPES = 'error', 'warning', 'notice'
        message_type = next(type_ for type_ in TYPES if type_ in message)

        return getattr(self, message_type)(**message)

    def system_error(self, msg_id=None, message=None, description=None,
                     validation_timeout=False, exc_info=None, **kw):
        """Add an error message for an unexpected exception in validator
        code, and move it to the front of the error message list. If
        `exc_info` is supplied, the error will be logged.

        If the error is a validation timeout, it is re-raised unless
        `msg_id` is "validation_timeout"."""

        if constants.IN_TESTS:
            # Exceptions that happen during tests should generally end the
            # test prematurely rather than just generating a message.
            raise exc_info[0], exc_info[1], exc_info[2]

        if (isinstance(exc_info[1], validator.ValidationTimeout) and
                msg_id != 'validation_timeout'):
            # These should always propagate to the top-level exception
            # handler, and be reported only once.
            raise exc_info[0], exc_info[1], exc_info[2]

        log.error('Unexpected error during validation: %s: %s'
                  % (exc_info[0].__name__, exc_info[1]),
                  exc_info=exc_info)

        full_id = ('validator', 'unexpected_exception')
        if msg_id:
            full_id += (msg_id,)

        self.error(full_id,
                   message or 'An unexpected error has occurred.',
                   description or
                   ('Validation was unable to complete successfully due '
                    'to an unexpected error.',
                    'The error has been logged, but please consider '
                    'filing an issue report here: '
                    'http://mzl.la/1DG0sFd'),
                   tier=1, **kw)

        # Move the error message to the beginning of the list.
        self.errors.insert(0, self.errors.pop())

    def drop_message(self, message):
        """Drop the given message object from the appropriate message list.

        Returns True if the message was found, otherwise False."""

        for type_ in 'errors', 'warnings', 'notices':
            list_ = getattr(self, type_)
            if message in list_:
                list_.remove(message)
                if 'signing_severity' in message:
                    self.signing_summary[message['signing_severity']] -= 1
                return True

        return False

    def set_tier(self, tier):
        'Updates the tier and ending tier'
        self.tier = tier
        if tier > self.ending_tier:
            self.ending_tier = tier

    @property
    def message_count(self):
        return len(self.errors) + len(self.warnings) + len(self.notices)

    def _save_message(self, stack, type_, message, context=None):
        """Store a message in the appropriate message stack."""

        uid = uuid.uuid4().hex
        message['uid'] = uid

        # Get the context for the message (if there's a context available)
        if context is not None:
            if isinstance(context, tuple):
                message['context'] = context
            else:
                message['context'] = (
                    context.get_context(line=message['line'],
                                        column=message['column']))
        else:
            message['context'] = None

        if self.package_stack:
            if not isinstance(message['file'], list):
                message['file'] = [message['file']]

            message['file'] = self.package_stack + message['file']

        # Test that if for_appversions is set that we're only applying to
        # supported add-ons. THIS IS THE LAST FILTER BEFORE THE MESSAGE IS
        # ADDED TO THE STACK!
        if message['for_appversions']:
            if not self.supports_version(message['for_appversions']):
                if self.instant:
                    print '(Instant error discarded)'
                    self._print_message(type_ + ': ', message, verbose=True)
                return
        elif self.version_requirements:
            # If there was no for_appversions but there were version
            # requirements detailed in the decorator, use the ones from the
            # decorator.
            message['for_appversions'] = self.version_requirements

        # Save the message to the stack.
        stack.append(message)

        # Mark the tier that the error occurred at.
        if message['tier'] is None:
            message['tier'] = self.tier

        # Build out the compatibility summary if possible.
        if message['compatibility_type']:
            self.compat_summary['%ss' % message['compatibility_type']] += 1

        # Build out the message tree entry.
        if message['id']:
            tree = self.message_tree
            last_id = None
            for eid in message['id']:
                if last_id is not None:
                    tree = tree[last_id]
                if eid not in tree:
                    tree[eid] = {'__errors': 0,
                                 '__warnings': 0,
                                 '__notices': 0,
                                 '__messages': []}
                tree[eid]['__%s' % type_] += 1
                last_id = eid

            tree[last_id]['__messages'].append(uid)

        # If instant mode is turned on, output the message immediately.
        if self.instant:
            self._print_message(type_ + ': ', message, verbose=True)

    def failed(self, fail_on_warnings=True):
        """Returns a boolean value describing whether the validation
        succeeded or not."""

        return bool(self.errors) or (fail_on_warnings and bool(self.warnings))

    def get_resource(self, name):
        'Retrieves an object that has been stored by another test.'

        if name in self.resources:
            return self.resources[name]
        elif name in self.pushable_resources:
            return self.pushable_resources[name]
        else:
            return False

    def save_resource(self, name, resource, pushable=False):
        'Saves an object such that it can be used by other tests.'

        if pushable:
            self.pushable_resources[name] = resource
        else:
            self.resources[name] = resource

    # Hack.
    def add_script_load(self, target, script):
        """Record the script `script` being loaded into the scope at
        `target`."""
        from collections import defaultdict
        from os.path import basename
        from urlparse import urlsplit

        scope_map = self.resources.setdefault('script_scopes',
                                              defaultdict(set))

        target = basename(target)
        script = basename(urlsplit(script).path)
        scope_map[target].add(script)

    @property
    def is_nested_package(self):
        'Returns whether the current package is within a PACKAGE_MULTI'
        return bool(self.package_stack)

    def push_state(self, new_file=''):
        'Saves the current error state to parse subpackages'

        self.subpackages.append({'detected_type': self.detected_type,
                                 'message_tree': self.message_tree,
                                 'resources': self.pushable_resources,
                                 'metadata': self.metadata})

        self.message_tree = {}
        self.pushable_resources = {}
        self.metadata = {'requires_chrome': False,
                         'listed': self.metadata.get('listed'),
                         'validator_version': validator.__version__}

        self.package_stack.append(new_file)

    def pop_state(self):
        'Retrieves the last saved state and restores it.'

        # Save a copy of the current state.
        state = self.subpackages.pop()
        metadata = self.metadata
        # We only rebuild message_tree anyway. No need to restore.

        # Copy the existing state back into place
        self.detected_type = state['detected_type']
        self.message_tree = state['message_tree']
        self.pushable_resources = state['resources']
        self.metadata = state['metadata']

        name = self.package_stack.pop()

        self.metadata.setdefault('sub_packages', {})[name] = metadata

    def render_json(self):
        'Returns a JSON summary of the validation operation.'

        types = {0: 'unknown',
                 1: 'extension',
                 2: 'theme',
                 3: 'dictionary',
                 4: 'langpack',
                 5: 'search',
                 8: 'webapp'}
        output = {'detected_type': types[self.detected_type],
                  'ending_tier': self.ending_tier,
                  'success': not self.failed(),
                  'messages': [],
                  'errors': len(self.errors),
                  'warnings': len(self.warnings),
                  'notices': len(self.notices),
                  'message_tree': self.message_tree,
                  'compatibility_summary': self.compat_summary,
                  'signing_summary': self.signing_summary,
                  'metadata': self.metadata}

        messages = output['messages']

        # Copy messages to the JSON output
        for error in self.errors:
            error['type'] = 'error'
            messages.append(error)

        for warning in self.warnings:
            warning['type'] = 'warning'
            messages.append(warning)

        for notice in self.notices:
            notice['type'] = 'notice'
            messages.append(notice)

        # Output the JSON.
        return json.dumps(output)

    def print_summary(self, verbose=False, no_color=False):
        'Prints a summary of the validation process so far.'

        types = {0: 'Unknown',
                 1: 'Extension/Multi-Extension',
                 2: 'Full Theme',
                 3: 'Dictionary',
                 4: 'Language Pack',
                 5: 'Search Provider',
                 7: 'Subpackage',
                 8: 'App'}
        detected_type = types[self.detected_type]

        buffer = StringIO()
        self.handler = OutputHandler(buffer, no_color)

        # Make a neat little printout.
        self.handler.write('\n<<GREEN>>Summary:') \
            .write('-' * 30) \
            .write('Detected type: <<BLUE>>%s' % detected_type) \
            .write('-' * 30)

        if self.failed():
            self.handler.write('<<BLUE>>Test failed! Errors:')

            # Print out all the errors/warnings:
            for error in self.errors:
                self._print_message('<<RED>>Error:<<NORMAL>>\t',
                                    error, verbose)
            for warning in self.warnings:
                self._print_message('<<YELLOW>>Warning:<<NORMAL>> ',
                                    warning, verbose)
        else:
            self.handler.write('<<GREEN>>All tests succeeded!')

        if self.notices:
            for notice in self.notices:
                self._print_message(prefix='<<WHITE>>Notice:<<NORMAL>>\t',
                                    message=notice,
                                    verbose=verbose)

        if 'is_jetpack' in self.metadata and verbose:
            self.handler.write('\n')
            self.handler.write('<<GREEN>>Jetpack add-on detected.<<NORMAL>>\n'
                               'Identified files:')
            if 'jetpack_identified_files' in self.metadata:
                for filename, data in \
                        self.metadata['jetpack_identified_files'].items():
                    self.handler.write((' %s\n' % filename) +
                                       ('  %s : %s' % data))

            if 'jetpack_unknown_files' in self.metadata:
                self.handler.write('Unknown files:')
                for filename in self.metadata['jetpack_unknown_files']:
                    self.handler.write(' %s' % filename)

        self.handler.write('\n')
        if self.unfinished:
            self.handler.write('<<RED>>Validation terminated early')
            self.handler.write('Errors during validation are preventing '
                               'the validation proecss from completing.')
            self.handler.write('Use the <<YELLOW>>--determined<<NORMAL>> '
                               'flag to ignore these errors.')
            self.handler.write('\n')

        return buffer.getvalue()

    def _flatten_list(self, data):
        'Flattens nested lists into strings.'

        if data is None:
            return ''
        if isinstance(data, types.StringTypes):
            return data
        elif isinstance(data, (list, tuple)):
            return '\n'.join(self._flatten_list(x) for x in data)

    def _print_message(self, prefix, message, verbose=True):
        'Prints a message and takes care of all sorts of nasty code'

        # Load up the standard output.
        output = ['\n', prefix, message['message']]

        # We have some extra stuff for verbose mode.
        if verbose:
            verbose_output = []

            # Detailed problem description.
            if message['description']:
                verbose_output.append(
                    self._flatten_list(message['description']))

            if message.get('signing_severity'):
                verbose_output.append(
                    ('\tAutomated signing severity: %s' %
                     message['signing_severity']))

            if message.get('signing_help'):
                verbose_output.append(
                    '\tSuggestions for passing automated signing:')
                verbose_output.append(
                    self._flatten_list(message['signing_help']))

            # Show the user what tier we're on
            verbose_output.append('\tTier:\t%d' % message['tier'])

            # If file information is available, output that as well.
            files = message['file']
            if files is not None and files != '':
                fmsg = '\tFile:\t%s'

                # Nested files (subpackes) are stored in a list.
                if type(files) is list:
                    if files[-1] == '':
                        files[-1] = '(none)'
                    verbose_output.append(fmsg % ' > '.join(files))
                else:
                    verbose_output.append(fmsg % files)

            # If there is a line number, that gets put on the end.
            if message['line']:
                verbose_output.append('\tLine:\t%s' % message['line'])
            if message['column'] and message['column'] != 0:
                verbose_output.append('\tColumn:\t%d' % message['column'])

            if message.get('context'):
                verbose_output.append('\tContext:')
                verbose_output.extend([('\t> %s' % x
                                        if x is not None
                                        else '\t>' + ('-' * 20))
                                       for x
                                       in message['context']])

            # Stick it in with the standard items.
            output.append('\n')
            output.append('\n'.join(verbose_output))

        # Send the final output to the handler to be rendered.
        self.handler.write(u''.join(map(unicodehelper.decode, output)))

    def supports_version(self, guid_set):
        """
        Returns whether a GUID set in for_appversions format is compatbile with
        the current supported applications list.
        """

        # Don't let the test run if we haven't parsed install.rdf yet.
        if self.supported_versions is None:
            raise Exception('Early compatibility test run before install.rdf '
                            'was parsed.')

        return self._compare_version(requirements=guid_set,
                                     support=self.supported_versions)

    def _compare_version(self, requirements, support):
        """
        Return whether there is an intersection between a support applications
        GUID set and a set of supported applications.
        """

        for guid in requirements:
            # If we support any of the GUIDs in the guid_set, test if any of
            # the provided versions for the GUID are supported.
            if (guid in support and
                any((detected_version in requirements[guid]) for
                    detected_version in support[guid])):
                return True

    def discard_unused_messages(self, ending_tier):
        """
        Delete messages from errors, warnings, and notices whose tier is
        greater than the ending tier.
        """

        stacks = [self.errors, self.warnings, self.notices]
        for stack in stacks:
            for message in stack:
                if message['tier'] > ending_tier:
                    stack.remove(message)
Example #9
0
    def print_summary(self, verbose=False, no_color=False):
        'Prints a summary of the validation process so far.'

        types = {0: 'Unknown',
                 1: 'Extension/Multi-Extension',
                 2: 'Full Theme',
                 3: 'Dictionary',
                 4: 'Language Pack',
                 5: 'Search Provider',
                 7: 'Subpackage',
                 8: 'App'}
        detected_type = types[self.detected_type]

        buffer = StringIO()
        self.handler = OutputHandler(buffer, no_color)

        # Make a neat little printout.
        self.handler.write('\n<<GREEN>>Summary:') \
            .write('-' * 30) \
            .write('Detected type: <<BLUE>>%s' % detected_type) \
            .write('-' * 30)

        if self.failed():
            self.handler.write('<<BLUE>>Test failed! Errors:')

            # Print out all the errors/warnings:
            for error in self.errors:
                self._print_message('<<RED>>Error:<<NORMAL>>\t',
                                    error, verbose)
            for warning in self.warnings:
                self._print_message('<<YELLOW>>Warning:<<NORMAL>> ',
                                    warning, verbose)
        else:
            self.handler.write('<<GREEN>>All tests succeeded!')

        if self.notices:
            for notice in self.notices:
                self._print_message(prefix='<<WHITE>>Notice:<<NORMAL>>\t',
                                    message=notice,
                                    verbose=verbose)

        if 'is_jetpack' in self.metadata and verbose:
            self.handler.write('\n')
            self.handler.write('<<GREEN>>Jetpack add-on detected.<<NORMAL>>\n'
                               'Identified files:')
            if 'jetpack_identified_files' in self.metadata:
                for filename, data in \
                        self.metadata['jetpack_identified_files'].items():
                    self.handler.write((' %s\n' % filename) +
                                       ('  %s : %s' % data))

            if 'jetpack_unknown_files' in self.metadata:
                self.handler.write('Unknown files:')
                for filename in self.metadata['jetpack_unknown_files']:
                    self.handler.write(' %s' % filename)

        self.handler.write('\n')
        if self.unfinished:
            self.handler.write('<<RED>>Validation terminated early')
            self.handler.write('Errors during validation are preventing '
                               'the validation proecss from completing.')
            self.handler.write('Use the <<YELLOW>>--determined<<NORMAL>> '
                               'flag to ignore these errors.')
            self.handler.write('\n')

        return buffer.getvalue()
Example #10
0
import sys
import os

from validator.constants import SPIDERMONKEY_INSTALLATION
from validator.errorbundler import ErrorBundle
from validator.outputhandlers.shellcolors import OutputHandler
import validator.testcases.scripting as scripting
import validator.testcases.javascript.traverser
from validator.testcases.javascript.predefinedentities import GLOBAL_ENTITIES
import validator.testcases.javascript.spidermonkey as spidermonkey
validator.testcases.javascript.traverser.DEBUG = True

if __name__ == '__main__':
    err = ErrorBundle(instant=True)
    err.handler = OutputHandler(sys.stdout, False)
    err.supported_versions = {}
    if len(sys.argv) > 1:
        path = sys.argv[1]
        script = open(path).read()
        scripting.test_js_file(err=err,
                               filename=path,
                               data=script)
    else:
        trav = validator.testcases.javascript.traverser.Traverser(err, "stdin")
        trav._push_context()

        def do_inspect(wrapper, arguments, traverser):
            print "~" * 50
            for arg in arguments:
                if arg["type"] == "Identifier":
Example #11
0
class ErrorBundle(object):
    """This class does all sorts of cool things. It gets passed around
    from test to test and collects up all the errors like the candy man
    'separating the sorrow and collecting up all the cream.' It's
    borderline magical.

    Keyword Arguments

    **determined**
        Whether the validator should continue after a tier fails
    **listed**
        True if the add-on is destined for AMO, false if not
    **instant**
        Who knows what this does
    **overrides**
        dict of install.rdf values to override. Possible keys:
        targetapp_minVersion, targetapp_maxVersion
    **for_appversions**
        A dict of app GUIDs referencing lists of versions. Determines which
        version-dependant tests should be run.
    """

    def __init__(self, determined=True, listed=True, instant=False, overrides=None, for_appversions=None, debug=0):

        self.handler = None

        self.debug_level = debug
        if debug == 0 and constants.IN_TESTS:
            self.debug_level = 1

        self.errors = []
        self.warnings = []
        self.notices = []
        self.message_tree = {}

        self.compat_summary = {"errors": 0, "warnings": 0, "notices": 0}
        self.signing_summary = {s: 0 for s in SIGNING_SEVERITIES}

        self.ending_tier = 1
        self.tier = 1

        self.subpackages = []
        self.package_stack = []

        self.detected_type = 0
        self.unfinished = False

        # TODO: Break off into resource helper
        self.resources = {}
        self.pushable_resources = {}
        self.final_context = None

        self.metadata = {"requires_chrome": False, "listed": listed, "validator_version": validator.__version__}
        if listed:
            self.resources["listed"] = True
        self.instant = instant
        self.determined = determined

        self.version_requirements = None

        self.overrides = overrides or None

        self.supported_versions = self.for_appversions = for_appversions

    def _message(type_, message_type):
        def wrap(self, *args, **kwargs):
            message = {"description": (), "file": "", "editors_only": False}

            if "location" in kwargs:
                loc = kwargs["location"]
                message.update({"file": loc.file, "line": loc.line, "column": loc.column})

            # Has to go.
            if "err_id" in kwargs:
                kwargs["id"] = kwargs["err_id"]
            if "filename" in kwargs:
                kwargs["file"] = kwargs["filename"]
            if message_type in kwargs:
                kwargs["message"] = kwargs[message_type]

            positional_args = ("id", "message", "description", "file", "line", "column")

            keys = positional_args + ("tier", "for_appversions", "compatibility_type", "editors_only", "context_data")

            # This is absurd.
            # Copy positional args into the kwargs dict, if they're missing.
            for key, arg in zip(positional_args, args):
                assert key not in kwargs
                kwargs[key] = arg

            for key in keys:
                message.setdefault(key, None)
                if key in kwargs:
                    message[key] = kwargs[key]

            if "signing_severity" in kwargs:
                severity = kwargs["signing_severity"]
                assert severity in SIGNING_SEVERITIES

                self.signing_summary[severity] += 1
                message["signing_severity"] = severity

            if "signing_help" in kwargs:
                message["signing_help"] = kwargs["signing_help"]

            self._save_message(getattr(self, type_), type_, message, context=kwargs.get("context"))
            return message

        wrap.__name__ = message_type
        return wrap

    # And then all the real functions. Ahh, how clean!
    error = _message("errors", "error")
    warning = _message("warnings", "warning")
    notice = _message("notices", "notice")

    def report(self, base_message, *messages):
        """Create a message from the given base message, updating it with
        properties from each message in a non-keyword argument, and report
        it to the correct reporting function.

        The correct reporting function is determined by the presence of either
        a "error", "warning", or "notice" key in the test properties, as
        expected by the so-named error bundle method.

        Example:

            `report({'err_id': ('javascript', 'dangerous_global', 'generic'),
                     'warning': 'Access to dangerous global',
                     'description': 'Evil. *hiss*'},
                    {'err_id': 'eval',
                     'warning': 'Do not use eval. Ugh.'})`

        Reports a new warning:

            `warning(err_id=('javascript', 'dangerous_global', 'eval'),
                     warning='Do not use eval. Ugh.',
                     description='Evil. *hiss*')`
        """

        # Merge additional message properties into the base message.
        message = reduce(merge_description, messages, base_message)

        # Get the message type based on which message key is included in the
        # properties.
        TYPES = "error", "warning", "notice"
        message_type = next(type_ for type_ in TYPES if type_ in message)

        return getattr(self, message_type)(**message)

    def system_error(self, msg_id=None, message=None, description=None, validation_timeout=False, exc_info=None, **kw):
        """Add an error message for an unexpected exception in validator
        code, and move it to the front of the error message list. If
        `exc_info` is supplied, the error will be logged.

        If the error is a validation timeout, it is re-raised unless
        `msg_id` is "validation_timeout"."""

        if constants.IN_TESTS:
            # Exceptions that happen during tests should generally end the
            # test prematurely rather than just generating a message.
            raise exc_info[0], exc_info[1], exc_info[2]

        if isinstance(exc_info[1], validator.ValidationTimeout) and msg_id != "validation_timeout":
            # These should always propagate to the top-level exception
            # handler, and be reported only once.
            raise exc_info[0], exc_info[1], exc_info[2]

        log.error("Unexpected error during validation: %s: %s" % (exc_info[0].__name__, exc_info[1]), exc_info=exc_info)

        full_id = ("validator", "unexpected_exception")
        if msg_id:
            full_id += (msg_id,)

        self.error(
            full_id,
            message or "An unexpected error has occurred.",
            description
            or (
                "Validation was unable to complete successfully due " "to an unexpected error.",
                "The error has been logged, but please consider "
                "filing an issue report here: "
                "http://mzl.la/1DG0sFd",
            ),
            tier=1,
            **kw
        )

        # Move the error message to the beginning of the list.
        self.errors.insert(0, self.errors.pop())

    def drop_message(self, message):
        """Drop the given message object from the appropriate message list.

        Returns True if the message was found, otherwise False."""

        for type_ in "errors", "warnings", "notices":
            list_ = getattr(self, type_)
            if message in list_:
                list_.remove(message)
                if "signing_severity" in message:
                    self.signing_summary[message["signing_severity"]] -= 1
                return True

        return False

    def set_tier(self, tier):
        "Updates the tier and ending tier"
        self.tier = tier
        if tier > self.ending_tier:
            self.ending_tier = tier

    @property
    def message_count(self):
        return len(self.errors) + len(self.warnings) + len(self.notices)

    def _save_message(self, stack, type_, message, context=None):
        """Store a message in the appropriate message stack."""

        uid = uuid.uuid4().hex
        message["uid"] = uid

        # Get the context for the message (if there's a context available)
        if context is not None:
            if isinstance(context, tuple):
                message["context"] = context
            else:
                message["context"] = context.get_context(line=message["line"], column=message["column"])
        else:
            message["context"] = None

        if self.package_stack:
            if not isinstance(message["file"], list):
                message["file"] = [message["file"]]

            message["file"] = self.package_stack + message["file"]

        # Test that if for_appversions is set that we're only applying to
        # supported add-ons. THIS IS THE LAST FILTER BEFORE THE MESSAGE IS
        # ADDED TO THE STACK!
        if message["for_appversions"]:
            if not self.supports_version(message["for_appversions"]):
                if self.instant:
                    print "(Instant error discarded)"
                    self._print_message(type_ + ": ", message, verbose=True)
                return
        elif self.version_requirements:
            # If there was no for_appversions but there were version
            # requirements detailed in the decorator, use the ones from the
            # decorator.
            message["for_appversions"] = self.version_requirements

        # Save the message to the stack.
        stack.append(message)

        # Mark the tier that the error occurred at.
        if message["tier"] is None:
            message["tier"] = self.tier

        # Build out the compatibility summary if possible.
        if message["compatibility_type"]:
            self.compat_summary["%ss" % message["compatibility_type"]] += 1

        # Build out the message tree entry.
        if message["id"]:
            tree = self.message_tree
            last_id = None
            for eid in message["id"]:
                if last_id is not None:
                    tree = tree[last_id]
                if eid not in tree:
                    tree[eid] = {"__errors": 0, "__warnings": 0, "__notices": 0, "__messages": []}
                tree[eid]["__%s" % type_] += 1
                last_id = eid

            tree[last_id]["__messages"].append(uid)

        # If instant mode is turned on, output the message immediately.
        if self.instant:
            self._print_message(type_ + ": ", message, verbose=True)

    def failed(self, fail_on_warnings=True):
        """Returns a boolean value describing whether the validation
        succeeded or not."""

        return bool(self.errors) or (fail_on_warnings and bool(self.warnings))

    def get_resource(self, name):
        "Retrieves an object that has been stored by another test."

        if name in self.resources:
            return self.resources[name]
        elif name in self.pushable_resources:
            return self.pushable_resources[name]
        else:
            return False

    def save_resource(self, name, resource, pushable=False):
        "Saves an object such that it can be used by other tests."

        if pushable:
            self.pushable_resources[name] = resource
        else:
            self.resources[name] = resource

    # Hack.
    def add_script_load(self, target, script):
        """Record the script `script` being loaded into the scope at
        `target`."""
        from collections import defaultdict
        from os.path import basename
        from urlparse import urlsplit

        scope_map = self.resources.setdefault("script_scopes", defaultdict(set))

        target = basename(target)
        script = basename(urlsplit(script).path)
        scope_map[target].add(script)

    @property
    def is_nested_package(self):
        "Returns whether the current package is within a PACKAGE_MULTI"
        return bool(self.package_stack)

    def push_state(self, new_file=""):
        "Saves the current error state to parse subpackages"

        self.subpackages.append(
            {
                "detected_type": self.detected_type,
                "message_tree": self.message_tree,
                "resources": self.pushable_resources,
                "metadata": self.metadata,
            }
        )

        self.message_tree = {}
        self.pushable_resources = {}
        self.metadata = {
            "requires_chrome": False,
            "listed": self.metadata.get("listed"),
            "validator_version": validator.__version__,
        }

        self.package_stack.append(new_file)

    def pop_state(self):
        "Retrieves the last saved state and restores it."

        # Save a copy of the current state.
        state = self.subpackages.pop()
        metadata = self.metadata
        # We only rebuild message_tree anyway. No need to restore.

        # Copy the existing state back into place
        self.detected_type = state["detected_type"]
        self.message_tree = state["message_tree"]
        self.pushable_resources = state["resources"]
        self.metadata = state["metadata"]

        name = self.package_stack.pop()

        self.metadata.setdefault("sub_packages", {})[name] = metadata

    def render_json(self):
        "Returns a JSON summary of the validation operation."

        types = {0: "unknown", 1: "extension", 2: "theme", 3: "dictionary", 4: "langpack", 5: "search", 8: "webapp"}
        output = {
            "detected_type": types[self.detected_type],
            "ending_tier": self.ending_tier,
            "success": not self.failed(),
            "messages": [],
            "errors": len(self.errors),
            "warnings": len(self.warnings),
            "notices": len(self.notices),
            "message_tree": self.message_tree,
            "compatibility_summary": self.compat_summary,
            "signing_summary": self.signing_summary,
            "metadata": self.metadata,
        }

        messages = output["messages"]

        # Copy messages to the JSON output
        for error in self.errors:
            error["type"] = "error"
            messages.append(error)

        for warning in self.warnings:
            warning["type"] = "warning"
            messages.append(warning)

        for notice in self.notices:
            notice["type"] = "notice"
            messages.append(notice)

        # Output the JSON.
        return json.dumps(output)

    def print_summary(self, verbose=False, no_color=False):
        "Prints a summary of the validation process so far."

        types = {
            0: "Unknown",
            1: "Extension/Multi-Extension",
            2: "Full Theme",
            3: "Dictionary",
            4: "Language Pack",
            5: "Search Provider",
            7: "Subpackage",
            8: "App",
        }
        detected_type = types[self.detected_type]

        buffer = StringIO()
        self.handler = OutputHandler(buffer, no_color)

        # Make a neat little printout.
        self.handler.write("\n<<GREEN>>Summary:").write("-" * 30).write(
            "Detected type: <<BLUE>>%s" % detected_type
        ).write("-" * 30)

        if self.failed():
            self.handler.write("<<BLUE>>Test failed! Errors:")

            # Print out all the errors/warnings:
            for error in self.errors:
                self._print_message("<<RED>>Error:<<NORMAL>>\t", error, verbose)
            for warning in self.warnings:
                self._print_message("<<YELLOW>>Warning:<<NORMAL>> ", warning, verbose)
        else:
            self.handler.write("<<GREEN>>All tests succeeded!")

        if self.notices:
            for notice in self.notices:
                self._print_message(prefix="<<WHITE>>Notice:<<NORMAL>>\t", message=notice, verbose=verbose)

        if "is_jetpack" in self.metadata and verbose:
            self.handler.write("\n")
            self.handler.write("<<GREEN>>Jetpack add-on detected.<<NORMAL>>\n" "Identified files:")
            if "jetpack_identified_files" in self.metadata:
                for filename, data in self.metadata["jetpack_identified_files"].items():
                    self.handler.write((" %s\n" % filename) + ("  %s : %s" % data))

            if "jetpack_unknown_files" in self.metadata:
                self.handler.write("Unknown files:")
                for filename in self.metadata["jetpack_unknown_files"]:
                    self.handler.write(" %s" % filename)

        self.handler.write("\n")
        if self.unfinished:
            self.handler.write("<<RED>>Validation terminated early")
            self.handler.write("Errors during validation are preventing " "the validation proecss from completing.")
            self.handler.write("Use the <<YELLOW>>--determined<<NORMAL>> " "flag to ignore these errors.")
            self.handler.write("\n")

        return buffer.getvalue()

    def _flatten_list(self, data):
        "Flattens nested lists into strings."

        if data is None:
            return ""
        if isinstance(data, types.StringTypes):
            return data
        elif isinstance(data, (list, tuple)):
            return "\n".join(self._flatten_list(x) for x in data)

    def _print_message(self, prefix, message, verbose=True):
        "Prints a message and takes care of all sorts of nasty code"

        # Load up the standard output.
        output = ["\n", prefix, message["message"]]

        # We have some extra stuff for verbose mode.
        if verbose:
            verbose_output = []

            # Detailed problem description.
            if message["description"]:
                verbose_output.append(self._flatten_list(message["description"]))

            if message.get("signing_severity"):
                verbose_output.append(("\tAutomated signing severity: %s" % message["signing_severity"]))

            if message.get("signing_help"):
                verbose_output.append("\tSuggestions for passing automated signing:")
                verbose_output.append(self._flatten_list(message["signing_help"]))

            # Show the user what tier we're on
            verbose_output.append("\tTier:\t%d" % message["tier"])

            # If file information is available, output that as well.
            files = message["file"]
            if files is not None and files != "":
                fmsg = "\tFile:\t%s"

                # Nested files (subpackes) are stored in a list.
                if type(files) is list:
                    if files[-1] == "":
                        files[-1] = "(none)"
                    verbose_output.append(fmsg % " > ".join(files))
                else:
                    verbose_output.append(fmsg % files)

            # If there is a line number, that gets put on the end.
            if message["line"]:
                verbose_output.append("\tLine:\t%s" % message["line"])
            if message["column"] and message["column"] != 0:
                verbose_output.append("\tColumn:\t%d" % message["column"])

            if message.get("context"):
                verbose_output.append("\tContext:")
                verbose_output.extend(
                    [("\t> %s" % x if x is not None else "\t>" + ("-" * 20)) for x in message["context"]]
                )

            # Stick it in with the standard items.
            output.append("\n")
            output.append("\n".join(verbose_output))

        # Send the final output to the handler to be rendered.
        self.handler.write(u"".join(map(unicodehelper.decode, output)))

    def supports_version(self, guid_set):
        """
        Returns whether a GUID set in for_appversions format is compatbile with
        the current supported applications list.
        """

        # Don't let the test run if we haven't parsed install.rdf yet.
        if self.supported_versions is None:
            raise Exception("Early compatibility test run before install.rdf " "was parsed.")

        return self._compare_version(requirements=guid_set, support=self.supported_versions)

    def _compare_version(self, requirements, support):
        """
        Return whether there is an intersection between a support applications
        GUID set and a set of supported applications.
        """

        for guid in requirements:
            # If we support any of the GUIDs in the guid_set, test if any of
            # the provided versions for the GUID are supported.
            if guid in support and any((detected_version in requirements[guid]) for detected_version in support[guid]):
                return True

    def discard_unused_messages(self, ending_tier):
        """
        Delete messages from errors, warnings, and notices whose tier is
        greater than the ending tier.
        """

        stacks = [self.errors, self.warnings, self.notices]
        for stack in stacks:
            for message in stack:
                if message["tier"] > ending_tier:
                    stack.remove(message)