|
|
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 |
|