In complex tests (KVM) an exception string is often not informative enough and
the traceback and source code have to be examined in order to figure out what
caused the exception.  Context strings are a way for tests to provide
information about what they're doing, so that when an exception is raised, this
information will be embedded in the exception string.  The result is a concise
yet highly informative exception string, which should make it very easy to
figure out where/when the exception was raised.

A typical example for a test where this may be useful is KVM's reboot test.
Some exceptions can be raised either before or after the VM is rebooted (e.g.
logging into the guest can fail) and whether they are raised before or after
is critical to the understanding of the failure.  Normally the traceback would
have to be examined, but the proposed method makes it easy to know where the
exception is raised without doing so.  To achieve this, the reboot test should
place calls to error.context() as follows:

error.context("before reboot")
<carry out pre-reboot actions>
error.context("sending reboot command")
<send the reboot command>
error.context("after reboot")
<carry out post-reboot actions>

If login fails in the pre-reboot section, the resulting exception string can
can have something like "context: before reboot" embedded in it.  (The actual
embedding is done in the next patch in the series.)

Differences from previous version:
- Allow for 2 context levels per function using base_context().  This may be
  useful for some functions.
- Update the comment in error.py.
- Rename context_for_exception() to exception_context().
- Add set_exception_context().
- Add join_contexts().

Signed-off-by: Michael Goldish <mgold...@redhat.com>
Signed-off-by: Eduardo Habkost <ehabk...@redhat.com>
---
 client/common_lib/error.py |  139 +++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 137 insertions(+), 2 deletions(-)

diff --git a/client/common_lib/error.py b/client/common_lib/error.py
index f1ddaea..b0e33b2 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,140 @@ 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[-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
-- 
1.7.3.4

--
To unsubscribe from this list: send the line "unsubscribe kvm" in
the body of a message to majord...@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html

Reply via email to