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 |