Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 45 additions & 11 deletions lib/net/imap/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,43 @@ class ResponseParseError < Error
ESC_NO_HL = Hash.new("").freeze
private_constant :ESC_NO_HL

# Translates hash[:"/foo"] to hash[:reset] when hash.key?(:foo), else ""
#
# TODO: DRY this up with Config::AttrTypeCoercion.safe
if defined?(::Ractor.shareable_proc)
default_highlight = Ractor.shareable_proc {|hash, key|
%r{\A/(.+)} =~ key && hash.key?($1.to_sym) ? hash[:reset] : ""
}
else
default_highlight = nil.instance_eval { Proc.new {|hash, key|
%r{\A/(.+)} =~ key && hash.key?($1.to_sym) ? hash[:reset] : ""
} }
::Ractor.make_shareable(default_highlight) if defined?(::Ractor)
end

# ANSI highlights, but no colors
ESC_NO_COLOR = Hash.new("").update(
ESC_NO_COLOR = Hash.new(&default_highlight).update(
reset: "\e[m",
val: "\e[1m", # bold
alt: "\e[1;4m", # bold and underlined
sym: "\e[1m", # bold
label: "\e[1m", # bold
).freeze
private_constant :ESC_NO_COLOR

# ANSI highlights, with color
ESC_COLORS = Hash.new(&default_highlight).update(
reset: "\e[m",
key: "\e[95m", # bright magenta
idx: "\e[34m", # blue
val: "\e[36;40m", # cyan on black (to ensure contrast)
alt: "\e[1;33;40m", # bold; yellow on black
sym: "\e[33;40m", # yellow on black
label: "\e[1m", # bold
nil: "\e[35m", # magenta
).freeze
private_constant :ESC_COLORS

# Net::IMAP::ResponseParser, unless a custom parser produced the error.
attr_reader :parser_class

Expand Down Expand Up @@ -119,22 +148,27 @@ def initialize(message = "unspecified parse error",
#
# When +highlight+ is not explicitly set, highlights may be enabled
# automatically, based on +TERM+ and +FORCE_COLOR+ environment variables.
#
# By default, +highlight+ uses colors from the basic ANSI palette. When
# +highlight_no_color+ is true or the +NO_COLOR+ environment variable is
# not empty, only monochromatic highlights are used: bold, underline, etc.
def detailed_message(parser_state: Net::IMAP.debug,
parser_backtrace: false,
highlight: default_highlight_from_env,
highlight_no_color: (ENV["NO_COLOR"] || "") != "",
**)
return super unless parser_state || parser_backtrace
msg = super.dup
esc = highlight ? ESC_NO_COLOR : ESC_NO_HL
esc = !highlight ? ESC_NO_HL : highlight_no_color ? ESC_NO_COLOR : ESC_COLORS
hl = ->str { str % esc }
val = ->str, val { val.nil? ? "nil" : str % esc % val }
val = ->str, val { hl[val.nil? ? "%{nil}%%p%{/nil}" : str] % val }
if parser_state && (string || pos || lex_state || token)
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}%%<symbol>p%{reset} => %{val}%%<value>p%{reset}", token&.to_h
msg << hl["\n %{key}processed %{/key}: "] << val["%{val}%%p%{/val}", processed_string]
msg << hl["\n %{key}remaining %{/key}: "] << val["%{alt}%%p%{/alt}", remaining_string]
msg << hl["\n %{key}pos %{/key}: "] << val["%{val}%%p%{/val}", pos]
msg << hl["\n %{key}lex_state %{/key}: "] << val["%{sym}%%p%{/sym}", lex_state]
msg << hl["\n %{key}token %{/key}: "] << val[
"%{sym}%%<symbol>p%{/sym} => %{val}%%<value>p%{/val}", token&.to_h
]
end
if parser_backtrace
Expand All @@ -147,8 +181,8 @@ def detailed_message(parser_state: Net::IMAP.debug,
next unless loc.path&.include?("net/imap/response_parser")
end
msg << "\n %s: %s (%s:%d)" % [
"caller[%2d]" % idx,
hl["%{val}%%-30s%{reset}"] % loc.base_label,
hl["%{key}caller[%{/key}%{idx}%%2d%{/idx}%{key}]%{/key}"] % idx,
hl["%{label}%%-30s%{/label}"] % loc.base_label,
File.basename(loc.path, ".rb"), loc.lineno
]
end
Expand Down
53 changes: 44 additions & 9 deletions test/net/imap/test_errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ def self.SGR(*attr) = CSI attr.join(?;), ?m
RESET = SGR "" # could also use 0
BOLD = SGR 1
BOLD_UNDERLINE = SGR 1, 4
BOLD_YELLOW = SGR 1, 33, 40
YELLOW = SGR 33, 40
BLUE = SGR 34
CYAN = SGR 36, 40
MAGENTA_DARK = SGR 35
MAGENTA = SGR 95

setup do
@term_env_vars = ENV["TERM"], ENV["NO_COLOR"], ENV["FORCE_COLOR"]
Expand Down Expand Up @@ -90,26 +96,41 @@ def self.SGR(*attr) = CSI attr.join(?;), ?m
lex_state : #{BOLD}:EXPR_BEG#{RESET}
token : #{BOLD}:QUOTED#{RESET} => #{BOLD}"Microsoft.Exchange.Error: foo"#{RESET}
MSG
expected_color_hl = <<~MSG.strip
#{BOLD}#{msg} (#{BOLD_UNDERLINE}#{name}#{RESET}#{BOLD})#{RESET}
#{MAGENTA}processed #{RESET}: #{CYAN}"tag OK [Error=\\"Microsoft.Exchange.Error: foo\\""#{RESET}
#{MAGENTA}remaining #{RESET}: #{BOLD_YELLOW}"] done\\r\\n"#{RESET}
#{MAGENTA}pos #{RESET}: #{CYAN }45#{RESET}
#{MAGENTA}lex_state #{RESET}: #{YELLOW}:EXPR_BEG#{RESET}
#{MAGENTA}token #{RESET}: #{YELLOW}:QUOTED#{RESET} => #{CYAN}"Microsoft.Exchange.Error: foo"#{RESET}
MSG

ENV["TERM"], ENV["NO_COLOR"], ENV["FORCE_COLOR"] = nil, nil, "0"
assert_equal(expected_no_hl, err.detailed_message)
assert_equal(expected_no_color, err.detailed_message(highlight: true))
assert_equal(expected_color_hl, err.detailed_message(highlight: true))
assert_equal(expected_no_color, err.detailed_message(highlight: true,
highlight_no_color: true))

ENV["TERM"], ENV["NO_COLOR"], ENV["FORCE_COLOR"] = "dumb", "1", nil
assert_equal(expected_no_hl, err.detailed_message)
assert_equal(expected_no_color, err.detailed_message(highlight: true))
assert_equal(expected_color_hl, err.detailed_message(highlight: true,
highlight_no_color: false))

ENV["TERM"], ENV["NO_COLOR"], ENV["FORCE_COLOR"] = "xterm", nil, nil
assert_equal(expected_no_color, err.detailed_message)
assert_equal(expected_color_hl, err.detailed_message)
assert_equal(expected_no_hl, err.detailed_message(highlight: false))
assert_equal(expected_no_color, err.detailed_message(highlight_no_color: true))

ENV["TERM"], ENV["NO_COLOR"], ENV["FORCE_COLOR"] = "dumb", nil, "1"
assert_equal(expected_no_color, err.detailed_message)
assert_equal(expected_color_hl, err.detailed_message)
assert_equal(expected_no_hl, err.detailed_message(highlight: false))
assert_equal(expected_no_color, err.detailed_message(highlight_no_color: true))

ENV["TERM"], ENV["NO_COLOR"], ENV["FORCE_COLOR"] = "unknown", "1", "1"
assert_equal(expected_no_color, err.detailed_message)
assert_equal(expected_no_hl, err.detailed_message(highlight: false))
assert_equal(expected_color_hl, err.detailed_message(highlight_no_color: false))

# reset to nil
ENV["TERM"], ENV["NO_COLOR"], ENV["FORCE_COLOR"] = nil, nil, nil
Expand All @@ -135,21 +156,35 @@ def self.SGR(*attr) = CSI attr.join(?;), ?m
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
#{MAGENTA}processed #{RESET}: #{CYAN}"tag OK [Error=\\"Microsoft.Exchange.Error: foo\\""#{RESET}
#{MAGENTA}remaining #{RESET}: #{BOLD_YELLOW}"] done\\r\\n"#{RESET}
#{MAGENTA}pos #{RESET}: #{CYAN }45#{RESET}
#{MAGENTA}lex_state #{RESET}: #{YELLOW}:EXPR_BEG#{RESET}
#{MAGENTA}token #{RESET}: #{MAGENTA_DARK}nil#{RESET}
MSG

# with parser_backtrace
Net::IMAP.debug = false
parser = Net::IMAP::ResponseParser.new
error = parser.parse("* 123 FETCH (UNKNOWN ...)\r\n") rescue $!
no_hl = error.detailed_message(parser_backtrace: true)
no_color = error.detailed_message(parser_backtrace: true, highlight: true)
color_hl = error.detailed_message(parser_backtrace: true, highlight: true)
no_color = error.detailed_message(parser_backtrace: true, highlight: true,
highlight_no_color: true)
assert_include no_hl, "caller[ 1]: %-30s (" % "msg_att"
assert_include no_color, "caller[ 1]: #{BOLD}%-30s#{RESET} (" % "msg_att"
assert_include color_hl,
"#{MAGENTA}caller[#{RESET}#{BLUE} 1#{RESET}#{MAGENTA}]#{RESET}: " \
"#{BOLD}%-30s#{RESET} (" % "msg_att"
end

if defined?(::Ractor)
%i[ESC_NO_HL ESC_NO_COLOR ESC_COLORS].each do |name|
test "ResponseParseError::#{name} is Ractor shareable" do
value = Net::IMAP::ResponseParseError.const_get(name)
assert Ractor.shareable? value
end
end
end

test "ResponseTooLargeError" do
Expand Down