Blame SOURCES/0009-python-Try-harder-to-print-the-full-traceback-on-err.patch

fe7475
From c713e3337d1227db68a4088096cd19ffed746e9f Mon Sep 17 00:00:00 2001
fe7475
From: "Richard W.M. Jones" <rjones@redhat.com>
fe7475
Date: Wed, 8 Aug 2018 13:50:23 +0100
fe7475
Subject: [PATCH] python: Try harder to print the full traceback on error.
fe7475
MIME-Version: 1.0
fe7475
Content-Type: text/plain; charset=UTF-8
fe7475
Content-Transfer-Encoding: 8bit
fe7475
fe7475
The tracebacks are compressed into a single line because we're using
fe7475
PyObject_Str, but they are just about usable if not very readable.
fe7475
For example you would see an error like this:
fe7475
fe7475
nbdkit: error: ./python-exception.py: config_complete: error: ['Traceback (most recent call last):\n', '  File "./python-exception.py", line 54, in config_complete\n    raise_error1()\n', '  File "./python-exception.py", line 48, in raise_error1\n    raise_error2()\n', '  File "./python-exception.py", line 45, in raise_error2\n    raise RuntimeError("this is the test string")\n', 'RuntimeError: this is the test string\n']
fe7475
fe7475
which can be read by manually unfolding the exception in an editor as:
fe7475
fe7475
nbdkit: error: ./python-exception.py: config_complete: error:
fe7475
Traceback (most recent call last):
fe7475
  File "./python-exception.py", line 54, in config_complete
fe7475
    raise_error1()
fe7475
  File "./python-exception.py", line 48, in raise_error1
fe7475
    raise_error2()
fe7475
  File "./python-exception.py", line 45, in raise_error2
fe7475
    raise RuntimeError("this is the test string")
fe7475
RuntimeError: this is the test string
fe7475
fe7475
This also fixes the Python exception test:
fe7475
fe7475
(1) It originally was not testing anything.  Adding ‘set -e’ fixes
fe7475
that.
fe7475
fe7475
(2) The valgrind test is always broken because of Python itself.
fe7475
Skip the test under valgrind.
fe7475
fe7475
(3) This now tests both simple exceptions and full tracebacks.
fe7475
fe7475
Tested with Python 2.7.15 & 3.6.6.
fe7475
fe7475
(cherry picked from commit 72c0d64a47db642cafa89884f2ee554bd0b8e822)
fe7475
---
fe7475
 plugins/python/python.c        | 93 +++++++++++++++++++++++++++++++++++-------
fe7475
 tests/python-exception.py      | 20 ++++++++-
fe7475
 tests/test-python-exception.sh | 20 ++++++++-
fe7475
 3 files changed, 117 insertions(+), 16 deletions(-)
fe7475
fe7475
diff --git a/plugins/python/python.c b/plugins/python/python.c
fe7475
index 7eb91d7..ef1a2cf 100644
fe7475
--- a/plugins/python/python.c
fe7475
+++ b/plugins/python/python.c
fe7475
@@ -129,27 +129,92 @@ python_to_string (PyObject *str)
fe7475
   return NULL;
fe7475
 }
fe7475
 
fe7475
+/* This is the fallback in case we cannot get the full traceback. */
fe7475
+static void
fe7475
+print_python_error (const char *callback, PyObject *error)
fe7475
+{
fe7475
+  PyObject *error_str;
fe7475
+  char *error_cstr = NULL;
fe7475
+
fe7475
+  error_str = PyObject_Str (error);
fe7475
+  error_cstr = python_to_string (error_str);
fe7475
+  nbdkit_error ("%s: %s: error: %s",
fe7475
+                script, callback,
fe7475
+                error_cstr ? error_cstr : "<unknown>");
fe7475
+  Py_DECREF (error_str);
fe7475
+  free (error_cstr);
fe7475
+}
fe7475
+
fe7475
+/* Convert the Python traceback to a string and call nbdkit_error.
fe7475
+ * https://stackoverflow.com/a/15907460/7126113
fe7475
+ */
fe7475
+static int
fe7475
+print_python_traceback (const char *callback,
fe7475
+                        PyObject *type, PyObject *error, PyObject *traceback)
fe7475
+{
fe7475
+  PyObject *module_name, *traceback_module, *format_exception_fn, *rv,
fe7475
+    *traceback_str;
fe7475
+  char *traceback_cstr;
fe7475
+
fe7475
+#ifdef HAVE_PYSTRING_FROMSTRING
fe7475
+  module_name = PyString_FromString ("traceback");
fe7475
+#else
fe7475
+  module_name = PyUnicode_FromString ("traceback");
fe7475
+#endif
fe7475
+  traceback_module = PyImport_Import (module_name);
fe7475
+  Py_DECREF (module_name);
fe7475
+
fe7475
+  /* couldn't 'import traceback' */
fe7475
+  if (traceback_module == NULL)
fe7475
+    return -1;
fe7475
+
fe7475
+  format_exception_fn = PyObject_GetAttrString (traceback_module,
fe7475
+                                                "format_exception");
fe7475
+  if (format_exception_fn == NULL)
fe7475
+    return -1;
fe7475
+  if (!PyCallable_Check (format_exception_fn))
fe7475
+    return -1;
fe7475
+
fe7475
+  rv = PyObject_CallFunctionObjArgs (format_exception_fn,
fe7475
+                                     type, error, traceback, NULL);
fe7475
+  traceback_str = PyObject_Str (rv);
fe7475
+  Py_DECREF (rv);
fe7475
+  traceback_cstr = python_to_string (traceback_str);
fe7475
+  if (traceback_cstr == NULL) {
fe7475
+    Py_DECREF (traceback_str);
fe7475
+    return -1;
fe7475
+  }
fe7475
+
fe7475
+  nbdkit_error ("%s: %s: error: %s",
fe7475
+                script, callback,
fe7475
+                traceback_cstr);
fe7475
+  Py_DECREF (traceback_str);
fe7475
+  free (traceback_cstr);
fe7475
+
fe7475
+  /* This means we succeeded in calling nbdkit_error. */
fe7475
+  return 0;
fe7475
+}
fe7475
+
fe7475
 static int
fe7475
 check_python_failure (const char *callback)
fe7475
 {
fe7475
   if (PyErr_Occurred ()) {
fe7475
-    PyObject *type, *error, *traceback, *error_str;
fe7475
-    char *error_cstr;
fe7475
+    PyObject *type, *error, *traceback;
fe7475
 
fe7475
-    /* Convert the Python exception to a string.
fe7475
-     * https://stackoverflow.com/a/1418703
fe7475
-     * But forget about the traceback, it's very hard to print.
fe7475
-     * https://stackoverflow.com/q/1796510
fe7475
-     */
fe7475
     PyErr_Fetch (&type, &error, &traceback);
fe7475
     PyErr_NormalizeException (&type, &error, &traceback);
fe7475
-    error_str = PyObject_Str (error);
fe7475
-    error_cstr = python_to_string (error_str);
fe7475
-    nbdkit_error ("%s: %s: error: %s",
fe7475
-                  script, callback,
fe7475
-                  error_cstr ? error_cstr : "<unknown>");
fe7475
-    Py_DECREF (error_str);
fe7475
-    free (error_cstr);
fe7475
+
fe7475
+    /* Try to print the full traceback. */
fe7475
+    if (print_python_traceback (callback, type, error, traceback) == -1) {
fe7475
+      /* Couldn't do that, so fall back to converting the Python error
fe7475
+       * to a string.
fe7475
+       */
fe7475
+      print_python_error (callback, error);
fe7475
+    }
fe7475
+
fe7475
+    /* In all cases this returns -1 to indicate that a Python error
fe7475
+     * occurred.
fe7475
+     */
fe7475
     return -1;
fe7475
   }
fe7475
   return 0;
fe7475
diff --git a/tests/python-exception.py b/tests/python-exception.py
fe7475
index 1debf51..739057f 100644
fe7475
--- a/tests/python-exception.py
fe7475
+++ b/tests/python-exception.py
fe7475
@@ -32,10 +32,28 @@
fe7475
 
fe7475
 # A dummy python plugin which just raises an exception in config_complete.
fe7475
 
fe7475
+test = "simple"
fe7475
 
fe7475
-def config_complete():
fe7475
+def config(k, v):
fe7475
+    global test
fe7475
+    if k == "test":
fe7475
+        test = v
fe7475
+    else:
fe7475
+        raise RuntimeError("unknown config parameter")
fe7475
+
fe7475
+def raise_error2():
fe7475
     raise RuntimeError("this is the test string")
fe7475
 
fe7475
+def raise_error1():
fe7475
+    raise_error2()
fe7475
+
fe7475
+def config_complete():
fe7475
+    if test == "simple":
fe7475
+        raise RuntimeError("this is the test string")
fe7475
+    elif test == "traceback":
fe7475
+        raise_error1()
fe7475
+    else:
fe7475
+        raise RuntimeError("unknown test")
fe7475
 
fe7475
 def open(readonly):
fe7475
     return 1
fe7475
diff --git a/tests/test-python-exception.sh b/tests/test-python-exception.sh
fe7475
index 83999af..fd94827 100755
fe7475
--- a/tests/test-python-exception.sh
fe7475
+++ b/tests/test-python-exception.sh
fe7475
@@ -31,12 +31,30 @@
fe7475
 # OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
fe7475
 # SUCH DAMAGE.
fe7475
 
fe7475
+set -e
fe7475
+set -x
fe7475
+
fe7475
+# Python language leaks like a sieve as well as a lot of worrying
fe7475
+# "Conditional jump or move depends on uninitialised value(s)".
fe7475
+if test -n "$NBDKIT_VALGRIND"; then
fe7475
+    echo "$0: skipping Python test under valgrind."
fe7475
+    exit 77
fe7475
+fi
fe7475
+
fe7475
 output=test-python-exception.out
fe7475
 
fe7475
 rm -f $output
fe7475
 
fe7475
-nbdkit -f -v python ./python-exception.py > $output 2>&1 ||:
fe7475
+nbdkit -f -v python ./python-exception.py test=simple > $output 2>&1 ||:
fe7475
+cat $output
fe7475
 
fe7475
 grep 'this is the test string' $output
fe7475
 
fe7475
+nbdkit -f -v python ./python-exception.py test=traceback > $output 2>&1 ||:
fe7475
+cat $output
fe7475
+
fe7475
+grep 'raise_error1' $output
fe7475
+grep 'raise_error2' $output
fe7475
+grep 'this is the test string' $output
fe7475
+
fe7475
 rm $output
fe7475
-- 
fe7475
1.8.3.1
fe7475