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<>Summary:") \ .write("-" * 30) \ .write("Detected type: <>%s" % detected_type) \ .write("-" * 30) if self.failed(): self.handler.write("<>Test failed! Errors:") # Print out all the errors/warnings: for error in self.errors: self._print_message("<>Error:<>\t", error, verbose) for warning in self.warnings: self._print_message("<>Warning:<> ", 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("<>All tests succeeded!") self._print_verbose(verbose) self.handler.write("\n") if self.unfinished: self.handler.write("<>Validation terminated early") self.handler.write("Errors during validation are preventing" "the validation proecss from completing.") self.handler.write("Use the <>--determined<> " "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 = "<>Notice:<>\t" for notice in self.notices: self._print_message(prefix=mesg, message=notice)