Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(883)

Unified Diff: client/common_lib/error.py

Issue 6246035: Merge remote branch 'cros/upstream' into master (Closed) Base URL: ssh://git@gitrw.chromium.org:9222/autotest.git@master
Patch Set: patch Created 9 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: client/common_lib/error.py
diff --git a/client/common_lib/error.py b/client/common_lib/error.py
index 42dfe2bca3d78266b4538f9d9a11d81f00a8d331..0c5641cd8b8cef51696abe655892bf2b22bebef3 100644
--- a/client/common_lib/error.py
+++ b/client/common_lib/error.py
@@ -2,13 +2,14 @@
Internal global error types
"""
-import sys, traceback
+import sys, traceback, threading, logging
from traceback import format_exception
# Add names you want to be imported by 'from errors import *' to this list.
# This must be list not a tuple as we modify it to include all of our
# the Exception classes we define below at the end of this file.
-__all__ = ['format_error']
+__all__ = ['format_error', 'context_aware', 'context', 'get_context',
+ 'exception_context']
def format_error():
@@ -21,6 +22,141 @@ def format_error():
return ''.join(trace)
+# Exception context information:
+# ------------------------------
+# Every function can have some context string associated with it.
+# The context string can be changed by calling context(str) and cleared by
+# calling context() with no parameters.
+# get_context() joins the current context strings of all functions in the
+# provided traceback. The result is a brief description of what the test was
+# doing in the provided traceback (which should be the traceback of a caught
+# exception).
+#
+# For example: assume a() calls b() and b() calls c().
+#
+# @error.context_aware
+# def a():
+# error.context("hello")
+# b()
+# error.context("world")
+# error.get_context() ----> 'world'
+#
+# @error.context_aware
+# def b():
+# error.context("foo")
+# c()
+#
+# @error.context_aware
+# def c():
+# error.context("bar")
+# error.get_context() ----> 'hello --> foo --> bar'
+#
+# The current context is automatically inserted into exceptions raised in
+# context_aware functions, so usually test code doesn't need to call
+# error.get_context().
+
+ctx = threading.local()
+
+
+def _new_context(s=""):
+ if not hasattr(ctx, "contexts"):
+ ctx.contexts = []
+ ctx.contexts.append(s)
+
+
+def _pop_context():
+ ctx.contexts.pop()
+
+
+def context(s="", log=None):
+ """
+ Set the context for the currently executing function and optionally log it.
+
+ @param s: A string. If not provided, the context for the current function
+ will be cleared.
+ @param log: A logging function to pass the context message to. If None, no
+ function will be called.
+ """
+ ctx.contexts[-1] = s
+ if s and log:
+ log("Context: %s" % get_context())
+
+
+def base_context(s="", log=None):
+ """
+ Set the base context for the currently executing function and optionally
+ log it. The base context is just another context level that is hidden by
+ default. Functions that require a single context level should not use
+ base_context().
+
+ @param s: A string. If not provided, the base context for the current
+ function will be cleared.
+ @param log: A logging function to pass the context message to. If None, no
+ function will be called.
+ """
+ ctx.contexts[-1] = ""
+ ctx.contexts[-2] = s
+ if s and log:
+ log("Context: %s" % get_context())
+
+
+def get_context():
+ """Return the current context (or None if none is defined)."""
+ if hasattr(ctx, "contexts"):
+ return " --> ".join([s for s in ctx.contexts if s])
+
+
+def exception_context(e):
+ """Return the context of a given exception (or None if none is defined)."""
+ if hasattr(e, "_context"):
+ return e._context
+
+
+def set_exception_context(e, s):
+ """Set the context of a given exception."""
+ e._context = s
+
+
+def join_contexts(s1, s2):
+ """Join two context strings."""
+ if s1:
+ if s2:
+ return "%s --> %s" % (s1, s2)
+ else:
+ return s1
+ else:
+ return s2
+
+
+def context_aware(fn):
+ """A decorator that must be applied to functions that call context()."""
+ def new_fn(*args, **kwargs):
+ _new_context()
+ _new_context("(%s)" % fn.__name__)
+ try:
+ try:
+ return fn(*args, **kwargs)
+ except Exception, e:
+ if not exception_context(e):
+ set_exception_context(e, get_context())
+ raise
+ finally:
+ _pop_context()
+ _pop_context()
+ new_fn.__name__ = fn.__name__
+ new_fn.__doc__ = fn.__doc__
+ new_fn.__dict__.update(fn.__dict__)
+ return new_fn
+
+
+def _context_message(e):
+ s = exception_context(e)
+ if s:
+ return " [context: %s]" % s
+ else:
+ return ""
+
+
class JobContinue(SystemExit):
"""Allow us to bail out requesting continuance."""
pass
@@ -33,7 +169,8 @@ class JobComplete(SystemExit):
class AutotestError(Exception):
"""The parent of all errors deliberatly thrown within the client code."""
- pass
+ def __str__(self):
+ return Exception.__str__(self) + _context_message(self)
class JobError(AutotestError):
@@ -46,10 +183,14 @@ class UnhandledJobError(JobError):
def __init__(self, unhandled_exception):
if isinstance(unhandled_exception, JobError):
JobError.__init__(self, *unhandled_exception.args)
+ elif isinstance(unhandled_exception, str):
+ JobError.__init__(self, unhandled_exception)
else:
msg = "Unhandled %s: %s"
msg %= (unhandled_exception.__class__.__name__,
unhandled_exception)
+ if not isinstance(unhandled_exception, AutotestError):
+ msg += _context_message(unhandled_exception)
msg += "\n" + traceback.format_exc()
JobError.__init__(self, msg)
@@ -87,10 +228,14 @@ class UnhandledTestError(TestError):
def __init__(self, unhandled_exception):
if isinstance(unhandled_exception, TestError):
TestError.__init__(self, *unhandled_exception.args)
+ elif isinstance(unhandled_exception, str):
+ TestError.__init__(self, unhandled_exception)
else:
msg = "Unhandled %s: %s"
msg %= (unhandled_exception.__class__.__name__,
unhandled_exception)
+ if not isinstance(unhandled_exception, AutotestError):
+ msg += _context_message(unhandled_exception)
msg += "\n" + traceback.format_exc()
TestError.__init__(self, msg)
@@ -100,10 +245,14 @@ class UnhandledTestFail(TestFail):
def __init__(self, unhandled_exception):
if isinstance(unhandled_exception, TestFail):
TestFail.__init__(self, *unhandled_exception.args)
+ elif isinstance(unhandled_exception, str):
+ TestFail.__init__(self, unhandled_exception)
else:
msg = "Unhandled %s: %s"
msg %= (unhandled_exception.__class__.__name__,
unhandled_exception)
+ if not isinstance(unhandled_exception, AutotestError):
+ msg += _context_message(unhandled_exception)
msg += "\n" + traceback.format_exc()
TestFail.__init__(self, msg)
@@ -118,7 +267,6 @@ class CmdError(TestError):
self.result_obj = result_obj
self.additional_text = additional_text
-
def __str__(self):
if self.result_obj.exit_status is None:
msg = "Command <%s> failed and is not responding to signals"
@@ -129,6 +277,7 @@ class CmdError(TestError):
if self.additional_text:
msg += ", " + self.additional_text
+ msg += _context_message(self)
msg += '\n' + repr(self.result_obj)
return msg

Powered by Google App Engine
This is Rietveld 408576698