diff --git a/lib/telebugs.rb b/lib/telebugs.rb index e85b038..0bfdb91 100644 --- a/lib/telebugs.rb +++ b/lib/telebugs.rb @@ -11,6 +11,8 @@ require_relative "telebugs/sender" require_relative "telebugs/wrapped_error" require_relative "telebugs/notice" +require_relative "telebugs/error_message" +require_relative "telebugs/backtrace" module Telebugs # The general error that this library uses when it wants to raise. diff --git a/lib/telebugs/backtrace.rb b/lib/telebugs/backtrace.rb new file mode 100644 index 0000000..68cf8b3 --- /dev/null +++ b/lib/telebugs/backtrace.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +module Telebugs + # Represents a cross-Ruby backtrace from exceptions (including JRuby Java + # exceptions). Provides information about stack frames (such as line number, + # file and method) in convenient for Telebugs format. + module Backtrace + module Patterns + # The pattern that matches standard Ruby stack frames, such as + # ./spec/notice_spec.rb:43:in `block (3 levels) in ' + RUBY = %r{\A + (?.+) # Matches './spec/notice_spec.rb' + : + (?\d+) # Matches '43' + :in\s + `(?.*)' # Matches "`block (3 levels) in '" + \z}x + + # The pattern that matches JRuby Java stack frames, such as + # org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105) + JAVA = %r{\A + (?.+) # Matches 'org.jruby.ast.NewlineNode.interpret' + \( + (? + (?:uri:classloader:/.+(?=:)) # Matches '/META-INF/jruby.home/protocol.rb' + | + (?:uri_3a_classloader_3a_.+(?=:)) # Matches 'uri_3a_classloader_3a_/gems/...' + | + [^:]+ # Matches 'NewlineNode.java' + ) + :? + (?\d+)? # Matches '105' + \) + \z}x + + # The pattern that tries to assume what a generic stack frame might look + # like, when exception's backtrace is set manually. + GENERIC = %r{\A + (?:from\s)? + (?.+) # Matches '/foo/bar/baz.ext' + : + (?\d+)? # Matches '43' or nothing + (?: + in\s`(?.+)' # Matches "in `func'" + | + :in\s(?.+) # Matches ":in func" + )? # ... or nothing + \z}x + + # The pattern that matches exceptions from PL/SQL such as + # ORA-06512: at "STORE.LI_LICENSES_PACK", line 1945 + # @note This is raised by https://github.com/kubo/ruby-oci8 + OCI = /\A + (?: + ORA-\d{5} + :\sat\s + (?:"(?.+)",\s)? + line\s(?\d+) + | + #{GENERIC} + ) + \z/x + + # The pattern that matches CoffeeScript backtraces usually coming from + # Rails & ExecJS + EXECJS = /\A + (?: + # Matches 'compile ((execjs):6692:19)' + (?.+)\s\((?.+):(?\d+):\d+\) + | + # Matches 'bootstrap_node.js:467:3' + (?.+):(?\d+):\d+(?) + | + # Matches the Ruby part of the backtrace + #{RUBY} + ) + \z/x + end + + def self.parse(error) + return [] if error.backtrace.nil? || error.backtrace.none? + + parse_backtrace(error) + end + + # Checks whether the given exception was generated by JRuby's VM. + def self.java_exception?(error) + if defined?(Java::JavaLang::Throwable) && + error.is_a?(Java::JavaLang::Throwable) + return true + end + + return false unless error.respond_to?(:backtrace) + + (Patterns::JAVA =~ error.backtrace.first) != nil + end + + class << self + private + + def best_regexp_for(error) + if java_exception?(error) + Patterns::JAVA + elsif oci_exception?(error) + Patterns::OCI + elsif execjs_exception?(error) + Patterns::EXECJS + else + Patterns::RUBY + end + end + + def oci_exception?(error) + defined?(OCIError) && error.is_a?(OCIError) + end + + def execjs_exception?(error) + return false unless defined?(ExecJS::RuntimeError) + return true if error.is_a?(ExecJS::RuntimeError) + return true if error.cause && error.cause.is_a?(ExecJS::RuntimeError) + + false + end + + def stack_frame(regexp, stackframe) + if (match = match_frame(regexp, stackframe)) + return { + file: match[:file], + line: (Integer(match[:line]) if match[:line]), + function: match[:function] + } + end + + {file: nil, line: nil, function: stackframe} + end + + def match_frame(regexp, stackframe) + match = regexp.match(stackframe) + return match if match + + Patterns::GENERIC.match(stackframe) + end + + def parse_backtrace(error) + regexp = best_regexp_for(error) + + error.backtrace.map.with_index do |stackframe, i| + frame = stack_frame(regexp, stackframe) + next(frame) unless frame[:file] + + frame + end + end + end + end +end diff --git a/lib/telebugs/error_message.rb b/lib/telebugs/error_message.rb new file mode 100644 index 0000000..e5794cd --- /dev/null +++ b/lib/telebugs/error_message.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Telebugs + # Parses error messages to make them more consistent. + module ErrorMessage + # On Ruby 3.1+, the error highlighting gem can produce messages that can + # span over multiple lines. We don't want to display multiline error titles. + # Therefore, we want to strip out the higlighting part so that the errors + # look consistent. + RUBY_31_ERROR_HIGHLIGHTING_DIVIDER = "\n\n" + + # The options for +String#encode+ + ENCODING_OPTIONS = {invalid: :replace, undef: :replace}.freeze + + def self.parse(error) + return unless (msg = error.message) + + msg.encode(Encoding::UTF_8, **ENCODING_OPTIONS) + .split(RUBY_31_ERROR_HIGHLIGHTING_DIVIDER) + .first + end + end +end diff --git a/lib/telebugs/notice.rb b/lib/telebugs/notice.rb index ab8a98c..e29eacd 100644 --- a/lib/telebugs/notice.rb +++ b/lib/telebugs/notice.rb @@ -13,15 +13,6 @@ class Notice Encoding::UndefinedConversionError ].freeze - # On Ruby 3.1+, the error highlighting gem can produce messages that can - # span over multiple lines. We don't want to display multiline error titles. - # Therefore, we want to strip out the higlighting part so that the errors - # look consistent. - RUBY_31_ERROR_HIGHLIGHTING_DIVIDER = "\n\n" - - # The options for +String#encode+ - ENCODING_OPTIONS = {invalid: :replace, undef: :replace}.freeze - # The maxium size of the JSON payload in bytes MAX_NOTICE_SIZE = 64000 @@ -54,19 +45,12 @@ def errors_as_json(error) WrappedError.new(error).unwrap.map do |e| { type: e.class.name, - message: message(e) + message: ErrorMessage.parse(e), + backtrace: Backtrace.parse(e) } end end - def message(error) - return unless (msg = error.message) - - msg.encode(Encoding::UTF_8, **ENCODING_OPTIONS) - .split(RUBY_31_ERROR_HIGHLIGHTING_DIVIDER) - .first - end - def truncate 0 end diff --git a/test/test_backtrace.rb b/test/test_backtrace.rb new file mode 100644 index 0000000..b628404 --- /dev/null +++ b/test/test_backtrace.rb @@ -0,0 +1,213 @@ +# frozen_string_literal: true + +require "test_helper" + +class TestBacktrace < Minitest::Test + def setup + @error = RuntimeError.new + end + + def test_parse_unix_backtrace + @error.set_backtrace([ + "/home/kyrylo/code/telebugs/ruby/spec/spec_helper.rb:23:in `'", + "/opt/rubies/ruby-2.2.2/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'", + "/opt/rubies/ruby-2.2.2/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'", + "/home/kyrylo/code/telebugs/ruby/spec/telebugs_spec.rb:1:in `'", + "/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/configuration.rb:1327:in `load'", + "/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/configuration.rb:1327:in `block in load_spec_files'", + "/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/configuration.rb:1325:in `each'", + "/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/configuration.rb:1325:in `load_spec_files'", + "/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/runner.rb:102:in `setup'", + "/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/runner.rb:88:in `run'", + "/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/runner.rb:73:in `run'", + "/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/runner.rb:41:in `invoke'", + "/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/exe/rspec:4:in `
'" + ]) + + parsed = [ + {file: "/home/kyrylo/code/telebugs/ruby/spec/spec_helper.rb", line: 23, function: ""}, + {file: "/opt/rubies/ruby-2.2.2/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb", line: 54, function: "require"}, + {file: "/opt/rubies/ruby-2.2.2/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb", line: 54, function: "require"}, + {file: "/home/kyrylo/code/telebugs/ruby/spec/telebugs_spec.rb", line: 1, function: ""}, + {file: "/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/configuration.rb", line: 1327, function: "load"}, + {file: "/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/configuration.rb", line: 1327, function: "block in load_spec_files"}, + {file: "/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/configuration.rb", line: 1325, function: "each"}, + {file: "/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/configuration.rb", line: 1325, function: "load_spec_files"}, + {file: "/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/runner.rb", line: 102, function: "setup"}, + {file: "/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/runner.rb", line: 88, function: "run"}, + {file: "/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/runner.rb", line: 73, function: "run"}, + {file: "/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/runner.rb", line: 41, function: "invoke"}, + {file: "/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/exe/rspec", line: 4, function: "
"} + ] + + assert_equal parsed, Telebugs::Backtrace.parse(@error) + end + + def test_parse_windows_backtrace + @error.set_backtrace([ + "C:/Program Files/Server/app/models/user.rb:13:in `magic'", + "C:/Program Files/Server/app/controllers/users_controller.rb:8:in `index'" + ]) + + parsed = [ + {:file=>"C:/Program Files/Server/app/models/user.rb", :line=>13, :function=>"magic"}, + {:file=>"C:/Program Files/Server/app/controllers/users_controller.rb", :line=>8, :function=>"index"} + ] + assert_equal parsed, Telebugs::Backtrace.parse(@error) + end + + def test_parse_jruby_java_backtrace + @error.set_backtrace([ + "org.jruby.java.invokers.InstanceMethodInvoker.call(InstanceMethodInvoker.java:26)", + "org.jruby.ir.interpreter.Interpreter.INTERPRET_EVAL(Interpreter.java:126)", + "org.jruby.RubyKernel$INVOKER$s$0$3$eval19.call(RubyKernel$INVOKER$s$0$3$eval19.gen)", + "org.jruby.RubyKernel$INVOKER$s$0$0$loop.call(RubyKernel$INVOKER$s$0$0$loop.gen)", + "org.jruby.runtime.IRBlockBody.doYield(IRBlockBody.java:139)", + "org.jruby.RubyKernel$INVOKER$s$rbCatch19.call(RubyKernel$INVOKER$s$rbCatch19.gen)", + "opt.rubies.jruby_minus_9_dot_0_dot_0_dot_0.bin.irb.invokeOther4:start(/opt/rubies/jruby-9.0.0.0/bin/irb)", + "opt.rubies.jruby_minus_9_dot_0_dot_0_dot_0.bin.irb.RUBY$script(/opt/rubies/jruby-9.0.0.0/bin/irb:13)", + "org.jruby.ir.Compiler$1.load(Compiler.java:111)", + "org.jruby.Main.run(Main.java:225)", + "org.jruby.Main.main(Main.java:197)" + ]) + + parsed = [ + {:file=>"InstanceMethodInvoker.java", :line=>26, :function=>"org.jruby.java.invokers.InstanceMethodInvoker.call"}, + {:file=>"Interpreter.java", :line=>126, :function=>"org.jruby.ir.interpreter.Interpreter.INTERPRET_EVAL"}, + {:file=>"RubyKernel$INVOKER$s$0$3$eval19.gen", :line=>nil, :function=>"org.jruby.RubyKernel$INVOKER$s$0$3$eval19.call"}, + {:file=>"RubyKernel$INVOKER$s$0$0$loop.gen", :line=>nil, :function=>"org.jruby.RubyKernel$INVOKER$s$0$0$loop.call"}, + {:file=>"IRBlockBody.java", :line=>139, :function=>"org.jruby.runtime.IRBlockBody.doYield"}, + {:file=>"RubyKernel$INVOKER$s$rbCatch19.gen", :line=>nil, :function=>"org.jruby.RubyKernel$INVOKER$s$rbCatch19.call"}, + {:file=>"/opt/rubies/jruby-9.0.0.0/bin/irb", :line=>nil, :function=>"opt.rubies.jruby_minus_9_dot_0_dot_0_dot_0.bin.irb.invokeOther4:start"}, + {:file=>"/opt/rubies/jruby-9.0.0.0/bin/irb", :line=>13, :function=>"opt.rubies.jruby_minus_9_dot_0_dot_0_dot_0.bin.irb.RUBY$script"}, + {:file=>"Compiler.java", :line=>111, :function=>"org.jruby.ir.Compiler$1.load"}, + {:file=>"Main.java", :line=>225, :function=>"org.jruby.Main.run"}, + {:file=>"Main.java", :line=>197, :function=>"org.jruby.Main.main"} + ] + assert_equal parsed, Telebugs::Backtrace.parse(@error) + end + + def test_parse_jruby_classloader_backtrace + @error.set_backtrace([ + 'uri_3a_classloader_3a_.META_minus_INF.jruby_dot_home.lib.ruby.stdlib.net.protocol.rbuf_fill(uri:classloader:/META-INF/jruby.home/lib/ruby/stdlib/net/protocol.rb:158)', + 'bin.processors.image_uploader.block in make_streams(bin/processors/image_uploader.rb:21)', + 'uri_3a_classloader_3a_.gems.faye_minus_websocket_minus_0_dot_10_dot_5.lib.faye.websocket.api.invokeOther13:dispatch_event(uri_3a_classloader_3a_/gems/faye_minus_websocket_minus_0_dot_10_dot_5/lib/faye/websocket/uri:classloader:/gems/faye-websocket-0.10.5/lib/faye/websocket/api.rb:109)', + 'tmp.jruby9022301782566983632extract.$dot.META_minus_INF.rails.file(/tmp/jruby9022301782566983632extract/./META-INF/rails.rb:13)' + ]) + + parsed = [ + { file: 'uri:classloader:/META-INF/jruby.home/lib/ruby/stdlib/net/protocol.rb', line: 158, function: 'uri_3a_classloader_3a_.META_minus_INF.jruby_dot_home.lib.ruby.stdlib.net.protocol.rbuf_fill' }, + { file: 'bin/processors/image_uploader.rb', line: 21, function: 'bin.processors.image_uploader.block in make_streams' }, + { file: 'uri_3a_classloader_3a_/gems/faye_minus_websocket_minus_0_dot_10_dot_5/lib/faye/websocket/uri:classloader:/gems/faye-websocket-0.10.5/lib/faye/websocket/api.rb', line: 109, function: 'uri_3a_classloader_3a_.gems.faye_minus_websocket_minus_0_dot_10_dot_5.lib.faye.websocket.api.invokeOther13:dispatch_event' }, + { file: '/tmp/jruby9022301782566983632extract/./META-INF/rails.rb', line: 13, function: 'tmp.jruby9022301782566983632extract.$dot.META_minus_INF.rails.file' } + ] + assert_equal parsed, Telebugs::Backtrace.parse(@error) + end + + def test_parse_jruby_nonthrowable_backtrace + @error.set_backtrace([ + 'org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(org/postgresql/core/v3/ConnectionFactoryImpl.java:257)', + 'org.postgresql.core.ConnectionFactory.openConnection(org/postgresql/core/ConnectionFactory.java:65)', + 'org.postgresql.jdbc2.AbstractJdbc2Connection.(org/postgresql/jdbc2/AbstractJdbc2Connection.java:149)' + ]) + + parsed = [ + { file: 'org/postgresql/core/v3/ConnectionFactoryImpl.java', line: 257, function: 'org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl' }, + { file: 'org/postgresql/core/ConnectionFactory.java', line: 65, function: 'org.postgresql.core.ConnectionFactory.openConnection' }, + { file: 'org/postgresql/jdbc2/AbstractJdbc2Connection.java', line: 149, function: 'org.postgresql.jdbc2.AbstractJdbc2Connection.' } + ] + assert_equal parsed, Telebugs::Backtrace.parse(@error) + end + + def test_parse_generic_backtrace_without_a_func + @error.set_backtrace([ + "/home/bingo/bango/assets/stylesheets/error_pages.scss:139:in `animation'", + "/home/bingo/bango/assets/stylesheets/error_pages.scss:139", + "/home/bingo/.gem/ruby/2.2.2/gems/sass-3.4.20/lib/sass/tree/visitors/perform.rb:349:in `block in visit_mixin'" + ]) + + parsed = [ + {:file=>"/home/bingo/bango/assets/stylesheets/error_pages.scss", :line=>139, :function=>"animation"}, + {:file=>"/home/bingo/bango/assets/stylesheets/error_pages.scss", :line=>139, :function=>nil}, + {:file=>"/home/bingo/.gem/ruby/2.2.2/gems/sass-3.4.20/lib/sass/tree/visitors/perform.rb", :line=>349, :function=>"block in visit_mixin"} + ] + assert_equal parsed, Telebugs::Backtrace.parse(@error) + end + + def test_parse_generic_backtrace_without_a_line_number + @error.set_backtrace([ + "/Users/grammakov/repositories/weintervene/config.ru:in `new'" + ]) + + parsed = [ + {:file=>"/Users/grammakov/repositories/weintervene/config.ru", :line=>nil, :function=>"new"} + ] + assert_equal parsed, Telebugs::Backtrace.parse(@error) + end + + def test_parse_unsupported_backtrace + @error.set_backtrace([ + "a b c 1 23 321 .rb" + ]) + + parsed = [ + file: nil, line: nil, function: 'a b c 1 23 321 .rb' + ] + assert_equal parsed, Telebugs::Backtrace.parse(@error) + end + + def test_parse_backtrace_with_an_empty_function + @error.set_backtrace([ + "/telebugs-ruby/vendor/jruby/1.9/gems/rspec-core-3.4.1/exe/rspec:3:in `'" + ]) + + parsed = [ + { file: '/telebugs-ruby/vendor/jruby/1.9/gems/rspec-core-3.4.1/exe/rspec', line: 3, function: '' } + ] + assert_equal parsed, Telebugs::Backtrace.parse(@error) + end + + def test_parse_oracle_backtrace + @error = OCIError.new + @error.set_backtrace([ + 'ORA-06512: at "STORE.LI_LICENSES_PACK", line 1945', + 'ORA-06512: at "ACTIVATION.LI_ACT_LICENSES_PACK", line 101', + 'ORA-06512: at line 2', + 'from stmt.c:243:in oci8lib_220.bundle' + ]) + + parsed = [ + { file: nil, line: 1945, function: 'STORE.LI_LICENSES_PACK' }, + { file: nil, line: 101, function: 'ACTIVATION.LI_ACT_LICENSES_PACK' }, + { file: nil, line: 2, function: nil }, + { file: 'stmt.c', line: 243, function: 'oci8lib_220.bundle' } + ] + assert_equal parsed, Telebugs::Backtrace.parse(@error) + end + + def test_parse_execjs_backtrace + @error = ExecJS::RuntimeError.new + @error.set_backtrace([ + 'compile ((execjs):6692:19)', + 'eval (:1:10)', + '(execjs):6703:8', + 'require../helpers.exports ((execjs):1:102)', + 'Object. ((execjs):1:120)', + 'Object.Module._extensions..js (module.js:550:10)', + 'bootstrap_node.js:467:3', + "/opt/rubies/ruby-2.3.1/lib/ruby/2.3.0/benchmark.rb:308:in `realtime'" + ]) + + parsed = [ + { file: '(execjs)', line: 6692, function: 'compile' }, + { file: '', line: 1, function: 'eval' }, + { file: '(execjs)', line: 6703, function: '' }, + { file: '(execjs)', line: 1, function: 'require../helpers.exports' }, + { file: '(execjs)', line: 1, function: 'Object.' }, + { file: 'module.js', line: 550, function: 'Object.Module._extensions..js' }, + { file: 'bootstrap_node.js', line: 467, function: '' }, + { file: '/opt/rubies/ruby-2.3.1/lib/ruby/2.3.0/benchmark.rb', line: 308, function: 'realtime' } + ] + assert_equal parsed, Telebugs::Backtrace.parse(@error) + end +end diff --git a/test/test_error_message.rb b/test/test_error_message.rb new file mode 100644 index 0000000..147a137 --- /dev/null +++ b/test/test_error_message.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "test_helper" + +class TestErrorMessage < Minitest::Test + def test_parse_when_error_message_is_nil + error = Class.new(StandardError) { + def message + end + }.new + + assert_nil Telebugs::ErrorMessage.parse(error) + end + + def test_parse_with_error_highlighting_in_messages + begin + raise "undefined method `[]' for nil:NilClass\n\n " \ + "data[:result].first[:first_name]\n ^^^^^^^^^^^^^" + rescue => e + end + + error_message = Telebugs::ErrorMessage.parse(e) + assert_equal "undefined method `[]' for nil:NilClass", error_message + end + + def test_parse_when_error_message_contains_invalid_characters + begin + JSON.parse(Marshal.dump(Time.now)) + rescue JSON::ParserError => e + end + + error_message = Telebugs::ErrorMessage.parse(e) + assert_match(/unexpected token at/, error_message) + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 230068c..ed18dd5 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -5,3 +5,8 @@ require "minitest/autorun" require "webmock/minitest" + +class OCIError < StandardError; end + +module ExecJS; end +class ExecJS::RuntimeError < StandardError; end diff --git a/test/test_notice.rb b/test/test_notice.rb index ebb8bb1..4b6f091 100644 --- a/test/test_notice.rb +++ b/test/test_notice.rb @@ -14,76 +14,18 @@ def test_to_json_with_nested_errors end end - assert_equal( - { - "errors" => [ - { - "type" => "StandardError", - "message" => "error 2" - }, - { - "type" => "StandardError", - "message" => "error 1" - } - ] - }, - JSON.parse(n.to_json) - ) - end - - def test_to_json_with_error_highlighting_in_messages - begin - raise "undefined method `[]' for nil:NilClass\n\n " \ - "data[:result].first[:first_name]\n ^^^^^^^^^^^^^" - rescue => e - end - - n = Telebugs::Notice.new(e) - - assert_equal( - { - "errors" => [ - { - "type" => "RuntimeError", - "message" => "undefined method `[]' for nil:NilClass" - } - ] - }, - JSON.parse(n.to_json) - ) - end - - def test_to_json_when_error_message_contains_invalid_characters - begin - JSON.parse(Marshal.dump(Time.now)) - rescue JSON::ParserError => e - end - - n = Telebugs::Notice.new(e) json = JSON.parse(n.to_json) - - assert_equal "JSON::ParserError", json["errors"].first["type"] - assert_match(/unexpected token at/, json["errors"].first["message"]) - end - - def test_to_json_when_error_message_is_nil - error = Class.new(StandardError) { - def message - end - }.new - - n = Telebugs::Notice.new(error) - - assert_equal( - { - "errors" => [ - { - "type" => nil, - "message" => nil - } - ] - }, - JSON.parse(n.to_json) - ) + error1 = json["errors"][0] + error2 = json["errors"][1] + + assert_equal "StandardError", error1["type"] + assert_equal "error 2", error1["message"] + assert error1.key?("backtrace") + assert error1["backtrace"].size > 0 + + assert_equal "StandardError", error2["type"] + assert_equal "error 1", error2["message"] + assert error2.key?("backtrace") + assert error2["backtrace"].size > 0 end end