app-validator/validator/errorbundler.py

369 строки
13 KiB
Python

import platform
import uuid
import json
if platform.system() != "Windows":
from outputhandlers.shellcolors import OutputHandler
else: # pragma: no cover
from outputhandlers.windowscolors import OutputHandler
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."""
def __init__(self, pipe=None, no_color=False, determined=True,
listed=False):
"""Specifying pipe allows the output of the bundler to be
written to a file rather than to the screen."""
self.errors = []
self.warnings = []
self.notices = []
self.message_tree = {}
self.tier = 0
self.metadata = {}
self.determined = determined
self.subpackages = []
self.package_stack = []
self.detected_type = 0
self.resources = {}
self.reject = False
self.unfinished = False
if listed:
self.resources["listed"] = True
self.handler = OutputHandler(pipe, no_color)
def error(self, err_id, error, description='', filename='', line=0):
"Stores an error message for the validation process"
self._save_message(self.errors,
"errors",
{"id": err_id,
"message": error,
"description": description,
"file": filename,
"line": line})
return self
def warning(self, err_id, warning, description='', filename='', line=0):
"Stores a warning message for the validation process"
self._save_message(self.warnings,
"warnings",
{"id": err_id,
"message": warning,
"description": description,
"file": filename,
"line": line})
return self
def info(self, err_id, info, description="", filename="", line=0):
"An alias for notice"
self.notice(err_id, info, description, filename, line)
def notice(self, err_id, notice, description="", filename="", line=0):
"Stores an informational message about the validation"
self._save_message(self.notices,
"notices",
{"id": err_id,
"message": notice,
"description": description,
"file": filename,
"line": line})
return self
def _save_message(self, stack, type_, message):
"Stores a message in the appropriate message stack."
uid = uuid.uuid1().hex
message["uid"] = uid
stack.append(message)
# Mark the tier that the error occurred at
message["tier"] = self.tier
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)
def set_type(self, type_):
"Stores the type of addon we're scanning"
self.detected_type = type_
def failed(self):
"""Returns a boolean value describing whether the validation
succeeded or not."""
return self.errors or self.warnings
def get_resource(self, name):
"Retrieves an object that has been stored by another test."
if not name in self.resources:
return False
return self.resources[name]
def save_resource(self, name, resource):
"Saves an object such that it can be used by other tests."
self.resources[name] = resource
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({"errors": self.errors,
"warnings": self.warnings,
"notices": self.notices,
"detected_type": self.detected_type,
"resources": self.resources,
"message_tree": self.message_tree})
self.errors = []
self.warnings = []
self.notices = []
self.resources = {}
self.message_tree = {}
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()
errors = self.errors
warnings = self.warnings
notices = self.notices
# We only rebuild message_tree anyway. No need to restore.
# Copy the existing state back into place
self.errors = state["errors"]
self.warnings = state["warnings"]
self.notices = state["notices"]
self.detected_type = state["detected_type"]
self.resources = state["resources"]
self.message_tree = state["message_tree"]
name = self.package_stack.pop()
self._merge_messages(errors, self.error, name)
self._merge_messages(warnings, self.warning, name)
self._merge_messages(notices, self.notice, name)
def _merge_messages(self, messages, callback, name):
"Merges a stack of messages into another stack of messages"
# Overlay the popped warnings onto the existing ones.
for message in messages:
trace = [name]
# If there are sub-sub-packages, they'll be in a list.
if isinstance(message["file"], list):
trace.extend(message["file"])
else:
trace.append(message["file"])
# Write the errors with the file structure delimited by
# right carets.
callback(message["id"],
message["message"],
message["description"],
trace,
message["line"])
def _clean_description(self, message, json=False):
"Cleans all the nasty whitespace from the descriptions."
output = self._clean_message(message["description"], json)
message["description"] = output
def _clean_message(self, desc, json=False):
"Cleans all the nasty whitespace from a string."
output = []
if not isinstance(desc, list):
lines = desc.splitlines()
for line in lines:
output.append(line.strip())
return " ".join(output)
else:
for line in desc:
output.append(self._clean_message(line, json))
if json:
return output
else:
return "\n".join(output)
def print_json(self, cluster=False):
"Prints a JSON summary of the validation operation."
types = {0: "unknown",
1: "extension",
2: "theme",
3: "dictionary",
4: "langpack",
5: "search"}
output = {"detected_type": types[self.detected_type],
"success": not self.failed(),
"rejected": self.reject,
"messages":[],
"errors": len(self.errors),
"warnings": len(self.warnings),
"notices": len(self.notices),
"message_tree": self.message_tree,
"metadata": self.metadata}
messages = output["messages"]
# Copy messages to the JSON output
for error in self.errors:
error["type"] = "error"
self._clean_description(error, True)
messages.append(error)
for warning in self.warnings:
warning["type"] = "warning"
self._clean_description(warning, True)
messages.append(warning)
for notice in self.notices:
notice["type"] = "notice"
self._clean_description(notice, True)
messages.append(notice)
# Output the JSON.
json_output = json.dumps(output)
self.handler.write(json_output)
def print_summary(self, verbose=False):
"Prints a summary of the validation process so far."
types = {0: "Unknown",
1: "Extension/Multi-Extension",
2: "Theme",
3: "Dictionary",
4: "Language Pack",
5: "Search Provider",
7: "Subpackage"}
detected_type = types[self.detected_type]
# 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)
# Prints things that only happen during verbose (infos).
self._print_verbose(verbose)
# Awwww... have some self esteem!
if self.reject:
self.handler.write("Extension Rejected")
else:
self.handler.write("<<GREEN>>All tests succeeded!")
self._print_verbose(verbose)
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")
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,
self._clean_message([message["message"]]),
"\n"]
# We have some extra stuff for verbose mode.
if verbose:
verbose_output = []
# Detailed problem description.
if message["description"]:
# These are dirty, so strip out whitespace and concat.
verbose_output.append(
self._clean_message(message["description"]))
# Show the user what tier we're on
verbose_output.append("\tTier: %d" % message["tier"])
# If file information is availe, 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"])
# 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(''.join(output))
def _print_verbose(self, verbose):
"Prints info code to help prevent code duplication"
if self.notices and verbose:
mesg = "<<WHITE>>Notice:<<NORMAL>>\t"
for notice in self.notices:
self._print_message(prefix=mesg, message=notice)