diff --git a/ext/quickjsrb/quickjsrb.c b/ext/quickjsrb/quickjsrb.c index 6d51250..80e4b7f 100644 --- a/ext/quickjsrb/quickjsrb.c +++ b/ext/quickjsrb/quickjsrb.c @@ -153,7 +153,8 @@ VALUE to_rb_value(JSContext *ctx, JSValue j_val) if (JS_IsError(ctx, j_val)) { VALUE r_maybe_ruby_error = find_ruby_error(ctx, j_val); - if (!NIL_P(r_maybe_ruby_error)) { + if (!NIL_P(r_maybe_ruby_error)) + { return r_maybe_ruby_error; } // will support other errors like just returning an instance of Error @@ -199,7 +200,8 @@ VALUE to_rb_value(JSContext *ctx, JSValue j_val) if (JS_IsError(ctx, j_exceptionVal)) { VALUE r_maybe_ruby_error = find_ruby_error(ctx, j_exceptionVal); - if (!NIL_P(r_maybe_ruby_error)) { + if (!NIL_P(r_maybe_ruby_error)) + { JS_FreeValue(ctx, j_exceptionVal); rb_exc_raise(r_maybe_ruby_error); return Qnil; @@ -211,8 +213,30 @@ VALUE to_rb_value(JSContext *ctx, JSValue j_val) JSValue j_errorClassMessage = JS_GetPropertyStr(ctx, j_exceptionVal, "message"); const char *errorClassMessage = JS_ToCString(ctx, j_errorClassMessage); + JSValue j_stackTrace = JS_GetPropertyStr(ctx, j_exceptionVal, "stack"); + const char *stackTrace = JS_ToCString(ctx, j_stackTrace); + const char *headlineTemplate = "Uncaught %s: %s\n%s"; + int length = snprintf(NULL, 0, headlineTemplate, errorClassName, errorClassMessage, stackTrace); + char *headline = (char *)malloc(length + 1); + snprintf(headline, length + 1, headlineTemplate, errorClassName, errorClassMessage, stackTrace); + + VMData *data = JS_GetContextOpaque(ctx); + VALUE r_log_class = rb_const_get(rb_const_get(rb_const_get(rb_cClass, rb_intern("Quickjs")), rb_intern("VM")), rb_intern("Log")); + VALUE r_log = rb_funcall(r_log_class, rb_intern("new"), 0); + rb_iv_set(r_log, "@severity", ID2SYM(rb_intern("error"))); + VALUE r_row = rb_ary_new(); + VALUE r_loghash = rb_hash_new(); + rb_hash_aset(r_loghash, ID2SYM(rb_intern("raw")), rb_str_new2(headline)); + rb_hash_aset(r_loghash, ID2SYM(rb_intern("c")), rb_str_new2(headline)); + rb_ary_push(r_row, r_loghash); + rb_iv_set(r_log, "@row", r_row); + rb_ary_push(data->logs, r_log); + JS_FreeValue(ctx, j_errorClassMessage); JS_FreeValue(ctx, j_errorClassName); + JS_FreeValue(ctx, j_stackTrace); + JS_FreeCString(ctx, stackTrace); + free(headline); VALUE r_error_class, r_error_message = rb_str_new2(errorClassMessage); if (is_native_error_name(errorClassName)) @@ -241,8 +265,26 @@ VALUE to_rb_value(JSContext *ctx, JSValue j_val) else // exception without Error object { const char *errorMessage = JS_ToCString(ctx, j_exceptionVal); - VALUE r_error_message = rb_sprintf("%s", errorMessage); + const char *headlineTemplate = "Uncaught '%s'"; + int length = snprintf(NULL, 0, headlineTemplate, errorMessage); + char *headline = (char *)malloc(length + 1); + snprintf(headline, length + 1, headlineTemplate, errorMessage); + + VMData *data = JS_GetContextOpaque(ctx); + VALUE r_log_class = rb_const_get(rb_const_get(rb_const_get(rb_cClass, rb_intern("Quickjs")), rb_intern("VM")), rb_intern("Log")); + VALUE r_log = rb_funcall(r_log_class, rb_intern("new"), 0); + rb_iv_set(r_log, "@severity", ID2SYM(rb_intern("error"))); + VALUE r_row = rb_ary_new(); + VALUE r_loghash = rb_hash_new(); + rb_hash_aset(r_loghash, ID2SYM(rb_intern("raw")), rb_str_new2(headline)); + rb_hash_aset(r_loghash, ID2SYM(rb_intern("c")), rb_str_new2(headline)); + rb_ary_push(r_row, r_loghash); + rb_iv_set(r_log, "@row", r_row); + rb_ary_push(data->logs, r_log); + + free(headline); + VALUE r_error_message = rb_sprintf("%s", errorMessage); JS_FreeCString(ctx, errorMessage); JS_FreeValue(ctx, j_exceptionVal); rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR), rb_intern("new"), 2, r_error_message, Qnil)); diff --git a/test/fixture.esm.js b/test/fixture.esm.js index 3812d81..f7ffe68 100644 --- a/test/fixture.esm.js +++ b/test/fixture.esm.js @@ -1,3 +1,11 @@ export const member = () => "I am a exported member of ESM."; export const defaultMember = () => "I am a default export of ESM."; export default defaultMember; + +const thrower = () => { + throw new Error("unpleasant wrapped error"); +} + +export const wrapError = () => { + thrower(); +} diff --git a/test/quickjs_test.rb b/test/quickjs_test.rb index 09554ed..100d81d 100644 --- a/test/quickjs_test.rb +++ b/test/quickjs_test.rb @@ -411,5 +411,61 @@ class ConsoleLoggers < QuickjsVmTest assert_equal(@vm.logs.last.raw, ['log promise', 'Promise']) end end + + class StackTraces < QuickjsVmTest + setup { @vm = Quickjs::VM.new } + teardown { @vm = nil } + + test 'unhandled exception with an Error class should be logged with stack trace' do + assert_raises(Quickjs::ReferenceError) do + @vm.eval_code(" + const a = 1; + const c = 3; + a + b; + ") + end + assert_equal(@vm.logs.size, 1) + assert_equal(@vm.logs.last.severity, :error) + assert_equal( + @vm.logs.last.raw.first.split("\n"), + [ + "Uncaught ReferenceError: 'b' is not defined", + ' at (:4)' + ] + ) + end + + test 'unhandled exception without any Error class should be logged with stack trace' do + assert_raises(Quickjs::RuntimeError) do + @vm.eval_code(" + const a = 1; + throw 'Don\\'t wanna compute at all'; + ") + end + assert_equal(@vm.logs.size, 1) + assert_equal(@vm.logs.last.severity, :error) + assert_equal( + @vm.logs.last.raw.first.split("\n"), + [ + "Uncaught 'Don't wanna compute at all'" + ] + ) + end + + test 'should include multi layers of stack trace' do + @vm.import(['wrapError'], from: File.read('./test/fixture.esm.js')) + assert_raises(Quickjs::RuntimeError) do + @vm.eval_code('wrapError();') + end + assert_equal(@vm.logs.size, 1) + assert_equal(@vm.logs.last.severity, :error) + trace = @vm.logs.last.raw.first.split("\n") + assert_equal(trace.size, 4) + assert_equal(trace[0], 'Uncaught Error: unpleasant wrapped error') + assert_match(/at thrower \(\w{12}:6\)/, trace[1]) + assert_match(/at wrapError \(\w{12}:10\)/, trace[2]) + assert_equal(' at ()', trace[3]) + end + end end end