diff --git a/lib/net/imap/errors.rb b/lib/net/imap/errors.rb index a64cab41..f7751d13 100644 --- a/lib/net/imap/errors.rb +++ b/lib/net/imap/errors.rb @@ -55,6 +55,18 @@ def response_size_msg # NOTE: Parser attributes are provided for debugging and inspection only. # Their names and semantics may change incompatibly in any release. class ResponseParseError < Error + # returns "" for all highlights + ESC_NO_HL = Hash.new("").freeze + private_constant :ESC_NO_HL + + # ANSI highlights, but no colors + ESC_NO_COLOR = Hash.new("").update( + reset: "\e[m", + val: "\e[1m", # bold + alt: "\e[1;4m", # bold and underlined + ).freeze + private_constant :ESC_NO_COLOR + # Net::IMAP::ResponseParser, unless a custom parser produced the error. attr_reader :parser_class @@ -106,20 +118,21 @@ def initialize(message = "unspecified parse error", # Most parser method names are based on rules in the IMAP grammar. def detailed_message(parser_state: Net::IMAP.debug, parser_backtrace: false, + highlight: false, **) return super unless parser_state || parser_backtrace msg = super.dup + esc = highlight ? ESC_NO_COLOR : ESC_NO_HL + hl = ->str { str % esc } + val = ->str, val { val.nil? ? "nil" : str % esc % val } if parser_state && (string || pos || lex_state || token) - msg << "\n processed : %p" % processed_string - msg << "\n remaining : %p" % remaining_string - msg << "\n pos : %p" % pos - msg << "\n lex_state : %p" % lex_state - msg << "\n token : " - if token - msg << "%p => %p" % [token.symbol, token.value] - else - msg << "nil" - end + msg << "\n processed : " << val["%{val}%%p%{reset}", processed_string] + msg << "\n remaining : " << val["%{alt}%%p%{reset}", remaining_string] + msg << "\n pos : " << val["%{val}%%p%{reset}", pos] + msg << "\n lex_state : " << val["%{val}%%p%{reset}", lex_state] + msg << "\n token : " << val[ + "%{val}%%p%{reset} => %{val}%%p%{reset}", token&.to_h + ] end if parser_backtrace backtrace_locations&.each_with_index do |loc, idx| @@ -130,11 +143,10 @@ def detailed_message(parser_state: Net::IMAP.debug, else next unless loc.path&.include?("net/imap/response_parser") end - msg << "\n caller[%2d]: %-30s (%s:%d)" % [ - idx, - loc.base_label, - File.basename(loc.path, ".rb"), - loc.lineno + msg << "\n %s: %s (%s:%d)" % [ + "caller[%2d]" % idx, + hl["%{val}%%-30s%{reset}"] % loc.base_label, + File.basename(loc.path, ".rb"), loc.lineno ] end end diff --git a/test/net/imap/test_errors.rb b/test/net/imap/test_errors.rb index 3c0b70e7..b9a83a2a 100644 --- a/test/net/imap/test_errors.rb +++ b/test/net/imap/test_errors.rb @@ -74,11 +74,11 @@ def self.SGR(*attr) = CSI attr.join(?;), ?m MSG assert_equal(<<~MSG.strip, err.detailed_message(highlight: true)) #{BOLD}#{msg} (#{BOLD_UNDERLINE}#{name}#{RESET}#{BOLD})#{RESET} - processed : "tag OK [Error=\\"Microsoft.Exchange.Error: foo\\"" - remaining : "] done\\r\\n" - pos : 45 - lex_state : :EXPR_BEG - token : :QUOTED => "Microsoft.Exchange.Error: foo" + processed : #{BOLD}"tag OK [Error=\\"Microsoft.Exchange.Error: foo\\""#{RESET} + remaining : #{BOLD_UNDERLINE}"] done\\r\\n"#{RESET} + pos : #{BOLD}45#{RESET} + lex_state : #{BOLD}:EXPR_BEG#{RESET} + token : #{BOLD}:QUOTED#{RESET} => #{BOLD}"Microsoft.Exchange.Error: foo"#{RESET} MSG # `parser_state` defaults to `Net::IMAP.debug`: @@ -89,6 +89,26 @@ def self.SGR(*attr) = CSI attr.join(?;), ?m err.detailed_message(highlight: true) ) + # with a nil token + parser_state = [string, :EXPR_BEG, 45, nil] + err = Net::IMAP::ResponseParseError.new(msg, string:, parser_state:) + assert_equal(<<~MSG.strip, err.detailed_message(parser_state: true)) + #{msg} (#{name}) + processed : "tag OK [Error=\\"Microsoft.Exchange.Error: foo\\"" + remaining : "] done\\r\\n" + pos : 45 + lex_state : :EXPR_BEG + token : nil + MSG + assert_equal(<<~MSG.strip, err.detailed_message(highlight: true, parser_state: true)) + #{BOLD}#{msg} (#{BOLD_UNDERLINE}#{name}#{RESET}#{BOLD})#{RESET} + processed : #{BOLD}"tag OK [Error=\\"Microsoft.Exchange.Error: foo\\""#{RESET} + remaining : #{BOLD_UNDERLINE}"] done\\r\\n"#{RESET} + pos : #{BOLD}45#{RESET} + lex_state : #{BOLD}:EXPR_BEG#{RESET} + token : nil + MSG + # with parser_backtrace Net::IMAP.debug = false parser = Net::IMAP::ResponseParser.new @@ -96,7 +116,7 @@ def self.SGR(*attr) = CSI attr.join(?;), ?m no_hl = error.detailed_message(parser_backtrace: true) no_color = error.detailed_message(parser_backtrace: true, highlight: true) assert_include no_hl, "caller[ 1]: %-30s (" % "msg_att" - assert_include no_color, "caller[ 1]: %-30s (" % "msg_att" + assert_include no_color, "caller[ 1]: #{BOLD}%-30s#{RESET} (" % "msg_att" end test "ResponseTooLargeError" do