Skip to content

Commit 93888de

Browse files
Merge pull request #296 from Distributive-Network/philippe/full-js-and-python-stacks
Python Trace supplement: Full JS stack trace details
2 parents 073972b + 9d737fd commit 93888de

File tree

5 files changed

+75
-20
lines changed

5 files changed

+75
-20
lines changed

include/setSpiderMonkeyException.hh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
*
1919
* @param cx - pointer to the JS context
2020
* @param exceptionStack - reference to the SpiderMonkey exception stack
21+
* @param printStack - whether or not to print the JS stack
2122
*/
22-
PyObject *getExceptionString(JSContext *cx, const JS::ExceptionStack &exceptionStack);
23+
PyObject *getExceptionString(JSContext *cx, const JS::ExceptionStack &exceptionStack, bool printStack);
2324

2425
/**
2526
* @brief This function sets a python error under the assumption that a JS_* function call has failed. Do not call this function if that is not the case.

src/ExceptionType.cc

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "include/setSpiderMonkeyException.hh"
1313

1414
#include "include/ExceptionType.hh"
15+
#include "include/StrType.hh"
1516

1617
#include <jsapi.h>
1718
#include <js/Exception.h>
@@ -26,7 +27,7 @@ ExceptionType::ExceptionType(JSContext *cx, JS::HandleObject error) {
2627
// Convert the JS Error object to a Python string
2728
JS::RootedValue errValue(cx, JS::ObjectValue(*error)); // err
2829
JS::RootedObject errStack(cx, JS::ExceptionStackOrNull(error)); // err.stack
29-
PyObject *errStr = getExceptionString(cx, JS::ExceptionStack(cx, errValue, errStack));
30+
PyObject *errStr = getExceptionString(cx, JS::ExceptionStack(cx, errValue, errStack), true);
3031

3132
// Construct a new SpiderMonkeyError python object
3233
// pyObject = SpiderMonkeyError(errStr)
@@ -78,6 +79,25 @@ tb_print_line_repeated(_PyUnicodeWriter *writer, long cnt)
7879
JSObject *ExceptionType::toJsError(JSContext *cx, PyObject *exceptionValue, PyObject *traceBack) {
7980
assert(exceptionValue != NULL);
8081

82+
// Gather JS context
83+
JS_ReportErrorASCII(cx, ""); // throw JS error and gather all details
84+
85+
JS::ExceptionStack exceptionStack(cx);
86+
if (!JS::GetPendingExceptionStack(cx, &exceptionStack)) {
87+
return NULL;
88+
}
89+
JS_ClearPendingException(cx);
90+
91+
std::stringstream stackStream;
92+
JS::RootedObject stackObj(cx, exceptionStack.stack());
93+
if (stackObj.get()) {
94+
JS::RootedString stackStr(cx);
95+
JS::BuildStackString(cx, nullptr, stackObj, &stackStr, 2, js::StackFormat::SpiderMonkey);
96+
stackStream << "JS Stack Trace:\n" << StrType(cx, stackStr).getValue();
97+
}
98+
99+
100+
// Gather Python context
81101
PyObject *pyErrType = PyObject_Type(exceptionValue);
82102
const char *pyErrTypeName = _PyType_Name((PyTypeObject *)pyErrType);
83103

@@ -235,12 +255,12 @@ JSObject *ExceptionType::toJsError(JSContext *cx, PyObject *exceptionValue, PyOb
235255
{
236256
std::stringstream msgStream;
237257
msgStream << "Python " << pyErrTypeName << ": " << PyUnicode_AsUTF8(pyErrMsg) << "\n" << PyUnicode_AsUTF8(_PyUnicodeWriter_Finish(&writer));
238-
std::string msg = msgStream.str();
258+
msgStream << stackStream.str();
239259

240260
JS::RootedValue rval(cx);
241261
JS::RootedString filename(cx, JS_NewStringCopyZ(cx, PyUnicode_AsUTF8(fileName)));
242-
JS::RootedString message(cx, JS_NewStringCopyZ(cx, msg.c_str()));
243-
// TODO stack argument cannot be passed in as a string anymore (deprecated), and could not find a proper example using the new argument type
262+
JS::RootedString message(cx, JS_NewStringCopyZ(cx, msgStream.str().c_str()));
263+
// stack argument cannot be passed in as a string anymore (deprecated), and could not find a proper example using the new argument type
244264
if (!JS::CreateError(cx, JSExnType::JSEXN_ERR, nullptr, filename, lineno, 0, nullptr, message, JS::NothingHandleValue, &rval)) {
245265
return NULL;
246266
}
@@ -256,15 +276,22 @@ JSObject *ExceptionType::toJsError(JSContext *cx, PyObject *exceptionValue, PyOb
256276
Py_XDECREF(code);
257277
}
258278

279+
// gather additional JS context details
280+
JS::ErrorReportBuilder reportBuilder(cx);
281+
if (!reportBuilder.init(cx, exceptionStack, JS::ErrorReportBuilder::WithSideEffects)) {
282+
return NULL;
283+
}
284+
JSErrorReport *errorReport = reportBuilder.report();
285+
259286
std::stringstream msgStream;
260287
msgStream << "Python " << pyErrTypeName << ": " << PyUnicode_AsUTF8(pyErrMsg);
261-
std::string msg = msgStream.str();
288+
msgStream << stackStream.str();
262289

263290
JS::RootedValue rval(cx);
264-
JS::RootedObject stack(cx);
265-
JS::RootedString filename(cx, JS_NewStringCopyZ(cx, "[python code]"));
266-
JS::RootedString message(cx, JS_NewStringCopyZ(cx, msg.c_str()));
267-
if (!JS::CreateError(cx, JSExnType::JSEXN_ERR, nullptr, filename, 0, 0, nullptr, message, JS::NothingHandleValue, &rval)) {
291+
JS::RootedString filename(cx, JS_NewStringCopyZ(cx, "")); // cannot be null or omitted, but is overriden by the errorReport
292+
JS::RootedString message(cx, JS_NewStringCopyZ(cx, msgStream.str().c_str()));
293+
// filename cannot be null
294+
if (!JS::CreateError(cx, JSExnType::JSEXN_ERR, nullptr, filename, 0, 0, errorReport, message, JS::NothingHandleValue, &rval)) {
268295
return NULL;
269296
}
270297

src/jsTypeFactory.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ void setPyException(JSContext *cx) {
288288
}
289289

290290
PyObject *type, *value, *traceback;
291-
PyErr_Fetch(&type, &value, &traceback); // also clears the error indicator
291+
PyErr_Fetch(&type, &value, &traceback);
292292

293293
JSObject *jsException = ExceptionType::toJsError(cx, value, traceback);
294294

src/setSpiderMonkeyException.cc

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
#include <codecvt>
1919
#include <locale>
2020

21-
PyObject *getExceptionString(JSContext *cx, const JS::ExceptionStack &exceptionStack) {
21+
PyObject *getExceptionString(JSContext *cx, const JS::ExceptionStack &exceptionStack, bool printStack) {
2222
JS::ErrorReportBuilder reportBuilder(cx);
2323
if (!reportBuilder.init(cx, exceptionStack, JS::ErrorReportBuilder::WithSideEffects /* may call the `toString` method if an object is thrown */)) {
2424
return PyUnicode_FromString("Spidermonkey set an exception, but could not initialize the error report.");
@@ -56,11 +56,13 @@ PyObject *getExceptionString(JSContext *cx, const JS::ExceptionStack &exceptionS
5656
// print out the SpiderMonkey error message
5757
outStrStream << reportBuilder.toStringResult().c_str() << "\n";
5858

59-
JS::HandleObject stackObj = exceptionStack.stack();
60-
if (stackObj) { // stack can be null
61-
JS::RootedString stackStr(cx);
62-
BuildStackString(cx, nullptr, stackObj, &stackStr, /* indent */ 2, js::StackFormat::SpiderMonkey);
63-
outStrStream << "Stack Trace: \n" << StrType(cx, stackStr).getValue();
59+
if (printStack) {
60+
JS::RootedObject stackObj(cx, exceptionStack.stack());
61+
if (stackObj.get()) {
62+
JS::RootedString stackStr(cx);
63+
BuildStackString(cx, nullptr, stackObj, &stackStr, 2, js::StackFormat::SpiderMonkey);
64+
outStrStream << "Stack Trace:\n" << StrType(cx, stackStr).getValue();
65+
}
6466
}
6567

6668
return PyUnicode_FromString(outStrStream.str().c_str());
@@ -79,11 +81,27 @@ void setSpiderMonkeyException(JSContext *cx) {
7981
PyErr_SetString(SpiderMonkeyError, "Spidermonkey set an exception, but was unable to retrieve it.");
8082
return;
8183
}
84+
85+
// check if it is a Python Exception and already has a stack trace
86+
bool printStack = true;
87+
88+
JS::RootedValue exn(cx);
89+
if (JS_GetPendingException(cx, &exn)) {
90+
if (exn.isObject()) {
91+
JS::RootedObject exnObj(cx, &exn.toObject());
92+
JS::RootedValue tmp(cx);
93+
if (JS_GetProperty(cx, exnObj, "message", &tmp) && tmp.isString()) {
94+
JS::RootedString rootedStr(cx, tmp.toString());
95+
printStack = strstr(JS_EncodeStringToUTF8(cx, rootedStr).get(), "JS Stack Trace") == NULL;
96+
}
97+
}
98+
}
99+
82100
JS_ClearPendingException(cx);
83101

84102
// `PyErr_SetString` uses `PyErr_SetObject` with `PyUnicode_FromString` under the hood
85103
// see https://github.com/python/cpython/blob/3.9/Python/errors.c#L234-L236
86-
PyObject *errStr = getExceptionString(cx, exceptionStack);
104+
PyObject *errStr = getExceptionString(cx, exceptionStack, printStack);
87105
PyErr_SetObject(SpiderMonkeyError, errStr);
88106
Py_XDECREF(errStr);
89107
}

tests/python/test_arrays.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -754,7 +754,12 @@ def myFunc(e,f):
754754
assert (False)
755755
except Exception as e:
756756
assert str(type(e)) == "<class 'pythonmonkey.SpiderMonkeyError'>"
757-
assert "object of type 'float' has no len()" in str(e)
757+
assert "object of type 'float' has no len()" in str(e)
758+
assert "test_arrays.py" in str(e)
759+
assert "line 751" in str(e)
760+
assert "in myFunc" in str(e)
761+
assert "JS Stack Trace" in str(e)
762+
assert "@evaluate:1:27" in str(e)
758763

759764
def test_sort_with_one_arg_keyfunc():
760765
items = ['Four', 'Three', 'One']
@@ -766,6 +771,8 @@ def myFunc(e):
766771
except Exception as e:
767772
assert str(type(e)) == "<class 'pythonmonkey.SpiderMonkeyError'>"
768773
assert "takes 1 positional argument but 2 were given" in str(e)
774+
assert "JS Stack Trace" in str(e)
775+
assert "@evaluate:1:27" in str(e)
769776

770777
def test_sort_with_builtin_keyfunc():
771778
items = ['Four', 'Three', 'One']
@@ -774,7 +781,9 @@ def test_sort_with_builtin_keyfunc():
774781
assert (False)
775782
except Exception as e:
776783
assert str(type(e)) == "<class 'pythonmonkey.SpiderMonkeyError'>"
777-
assert "len() takes exactly one argument (2 given)" in str(e)
784+
assert "len() takes exactly one argument (2 given)" in str(e)
785+
assert "JS Stack Trace" in str(e)
786+
assert "@evaluate:1:27" in str(e)
778787

779788
def test_sort_with_js_func():
780789
items = ['Four', 'Three', 'One']

0 commit comments

Comments
 (0)