Skip to content

Commit 094d616

Browse files
authored
🔀 Merge pull request #609 from ruby/response_parse_error-detailed_message-color-highlights
🥅💄 Add color highlights to parse error details (default honors `NO_COLOR`)
2 parents 8ab4cdf + 8ed8949 commit 094d616

2 files changed

Lines changed: 89 additions & 20 deletions

File tree

‎lib/net/imap/errors.rb‎

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,43 @@ class ResponseParseError < Error
5959
ESC_NO_HL = Hash.new("").freeze
6060
private_constant :ESC_NO_HL
6161

62+
# Translates hash[:"/foo"] to hash[:reset] when hash.key?(:foo), else ""
63+
#
64+
# TODO: DRY this up with Config::AttrTypeCoercion.safe
65+
if defined?(::Ractor.shareable_proc)
66+
default_highlight = Ractor.shareable_proc {|hash, key|
67+
%r{\A/(.+)} =~ key && hash.key?($1.to_sym) ? hash[:reset] : ""
68+
}
69+
else
70+
default_highlight = nil.instance_eval { Proc.new {|hash, key|
71+
%r{\A/(.+)} =~ key && hash.key?($1.to_sym) ? hash[:reset] : ""
72+
} }
73+
::Ractor.make_shareable(default_highlight) if defined?(::Ractor)
74+
end
75+
6276
# ANSI highlights, but no colors
63-
ESC_NO_COLOR = Hash.new("").update(
77+
ESC_NO_COLOR = Hash.new(&default_highlight).update(
6478
reset: "\e[m",
6579
val: "\e[1m", # bold
6680
alt: "\e[1;4m", # bold and underlined
81+
sym: "\e[1m", # bold
82+
label: "\e[1m", # bold
6783
).freeze
6884
private_constant :ESC_NO_COLOR
6985

86+
# ANSI highlights, with color
87+
ESC_COLORS = Hash.new(&default_highlight).update(
88+
reset: "\e[m",
89+
key: "\e[95m", # bright magenta
90+
idx: "\e[34m", # blue
91+
val: "\e[36;40m", # cyan on black (to ensure contrast)
92+
alt: "\e[1;33;40m", # bold; yellow on black
93+
sym: "\e[33;40m", # yellow on black
94+
label: "\e[1m", # bold
95+
nil: "\e[35m", # magenta
96+
).freeze
97+
private_constant :ESC_COLORS
98+
7099
# Net::IMAP::ResponseParser, unless a custom parser produced the error.
71100
attr_reader :parser_class
72101

@@ -119,22 +148,27 @@ def initialize(message = "unspecified parse error",
119148
#
120149
# When +highlight+ is not explicitly set, highlights may be enabled
121150
# automatically, based on +TERM+ and +FORCE_COLOR+ environment variables.
151+
#
152+
# By default, +highlight+ uses colors from the basic ANSI palette. When
153+
# +highlight_no_color+ is true or the +NO_COLOR+ environment variable is
154+
# not empty, only monochromatic highlights are used: bold, underline, etc.
122155
def detailed_message(parser_state: Net::IMAP.debug,
123156
parser_backtrace: false,
124157
highlight: default_highlight_from_env,
158+
highlight_no_color: (ENV["NO_COLOR"] || "") != "",
125159
**)
126160
return super unless parser_state || parser_backtrace
127161
msg = super.dup
128-
esc = highlight ? ESC_NO_COLOR : ESC_NO_HL
162+
esc = !highlight ? ESC_NO_HL : highlight_no_color ? ESC_NO_COLOR : ESC_COLORS
129163
hl = ->str { str % esc }
130-
val = ->str, val { val.nil? ? "nil" : str % esc % val }
164+
val = ->str, val { hl[val.nil? ? "%{nil}%%p%{/nil}" : str] % val }
131165
if parser_state && (string || pos || lex_state || token)
132-
msg << "\n processed : " << val["%{val}%%p%{reset}", processed_string]
133-
msg << "\n remaining : " << val["%{alt}%%p%{reset}", remaining_string]
134-
msg << "\n pos : " << val["%{val}%%p%{reset}", pos]
135-
msg << "\n lex_state : " << val["%{val}%%p%{reset}", lex_state]
136-
msg << "\n token : " << val[
137-
"%{val}%%<symbol>p%{reset} => %{val}%%<value>p%{reset}", token&.to_h
166+
msg << hl["\n %{key}processed %{/key}: "] << val["%{val}%%p%{/val}", processed_string]
167+
msg << hl["\n %{key}remaining %{/key}: "] << val["%{alt}%%p%{/alt}", remaining_string]
168+
msg << hl["\n %{key}pos %{/key}: "] << val["%{val}%%p%{/val}", pos]
169+
msg << hl["\n %{key}lex_state %{/key}: "] << val["%{sym}%%p%{/sym}", lex_state]
170+
msg << hl["\n %{key}token %{/key}: "] << val[
171+
"%{sym}%%<symbol>p%{/sym} => %{val}%%<value>p%{/val}", token&.to_h
138172
]
139173
end
140174
if parser_backtrace
@@ -147,8 +181,8 @@ def detailed_message(parser_state: Net::IMAP.debug,
147181
next unless loc.path&.include?("net/imap/response_parser")
148182
end
149183
msg << "\n %s: %s (%s:%d)" % [
150-
"caller[%2d]" % idx,
151-
hl["%{val}%%-30s%{reset}"] % loc.base_label,
184+
hl["%{key}caller[%{/key}%{idx}%%2d%{/idx}%{key}]%{/key}"] % idx,
185+
hl["%{label}%%-30s%{/label}"] % loc.base_label,
152186
File.basename(loc.path, ".rb"), loc.lineno
153187
]
154188
end

‎test/net/imap/test_errors.rb‎

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ def self.SGR(*attr) = CSI attr.join(?;), ?m
1111
RESET = SGR "" # could also use 0
1212
BOLD = SGR 1
1313
BOLD_UNDERLINE = SGR 1, 4
14+
BOLD_YELLOW = SGR 1, 33, 40
15+
YELLOW = SGR 33, 40
16+
BLUE = SGR 34
17+
CYAN = SGR 36, 40
18+
MAGENTA_DARK = SGR 35
19+
MAGENTA = SGR 95
1420

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

94108
ENV["TERM"], ENV["NO_COLOR"], ENV["FORCE_COLOR"] = nil, nil, "0"
95109
assert_equal(expected_no_hl, err.detailed_message)
96-
assert_equal(expected_no_color, err.detailed_message(highlight: true))
110+
assert_equal(expected_color_hl, err.detailed_message(highlight: true))
111+
assert_equal(expected_no_color, err.detailed_message(highlight: true,
112+
highlight_no_color: true))
97113

98114
ENV["TERM"], ENV["NO_COLOR"], ENV["FORCE_COLOR"] = "dumb", "1", nil
99115
assert_equal(expected_no_hl, err.detailed_message)
100116
assert_equal(expected_no_color, err.detailed_message(highlight: true))
117+
assert_equal(expected_color_hl, err.detailed_message(highlight: true,
118+
highlight_no_color: false))
101119

102120
ENV["TERM"], ENV["NO_COLOR"], ENV["FORCE_COLOR"] = "xterm", nil, nil
103-
assert_equal(expected_no_color, err.detailed_message)
121+
assert_equal(expected_color_hl, err.detailed_message)
104122
assert_equal(expected_no_hl, err.detailed_message(highlight: false))
123+
assert_equal(expected_no_color, err.detailed_message(highlight_no_color: true))
105124

106125
ENV["TERM"], ENV["NO_COLOR"], ENV["FORCE_COLOR"] = "dumb", nil, "1"
107-
assert_equal(expected_no_color, err.detailed_message)
126+
assert_equal(expected_color_hl, err.detailed_message)
108127
assert_equal(expected_no_hl, err.detailed_message(highlight: false))
128+
assert_equal(expected_no_color, err.detailed_message(highlight_no_color: true))
109129

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

114135
# reset to nil
115136
ENV["TERM"], ENV["NO_COLOR"], ENV["FORCE_COLOR"] = nil, nil, nil
@@ -135,21 +156,35 @@ def self.SGR(*attr) = CSI attr.join(?;), ?m
135156
MSG
136157
assert_equal(<<~MSG.strip, err.detailed_message(highlight: true, parser_state: true))
137158
#{BOLD}#{msg} (#{BOLD_UNDERLINE}#{name}#{RESET}#{BOLD})#{RESET}
138-
processed : #{BOLD}"tag OK [Error=\\"Microsoft.Exchange.Error: foo\\""#{RESET}
139-
remaining : #{BOLD_UNDERLINE}"] done\\r\\n"#{RESET}
140-
pos : #{BOLD}45#{RESET}
141-
lex_state : #{BOLD}:EXPR_BEG#{RESET}
142-
token : nil
159+
#{MAGENTA}processed #{RESET}: #{CYAN}"tag OK [Error=\\"Microsoft.Exchange.Error: foo\\""#{RESET}
160+
#{MAGENTA}remaining #{RESET}: #{BOLD_YELLOW}"] done\\r\\n"#{RESET}
161+
#{MAGENTA}pos #{RESET}: #{CYAN }45#{RESET}
162+
#{MAGENTA}lex_state #{RESET}: #{YELLOW}:EXPR_BEG#{RESET}
163+
#{MAGENTA}token #{RESET}: #{MAGENTA_DARK}nil#{RESET}
143164
MSG
144165

145166
# with parser_backtrace
146167
Net::IMAP.debug = false
147168
parser = Net::IMAP::ResponseParser.new
148169
error = parser.parse("* 123 FETCH (UNKNOWN ...)\r\n") rescue $!
149170
no_hl = error.detailed_message(parser_backtrace: true)
150-
no_color = error.detailed_message(parser_backtrace: true, highlight: true)
171+
color_hl = error.detailed_message(parser_backtrace: true, highlight: true)
172+
no_color = error.detailed_message(parser_backtrace: true, highlight: true,
173+
highlight_no_color: true)
151174
assert_include no_hl, "caller[ 1]: %-30s (" % "msg_att"
152175
assert_include no_color, "caller[ 1]: #{BOLD}%-30s#{RESET} (" % "msg_att"
176+
assert_include color_hl,
177+
"#{MAGENTA}caller[#{RESET}#{BLUE} 1#{RESET}#{MAGENTA}]#{RESET}: " \
178+
"#{BOLD}%-30s#{RESET} (" % "msg_att"
179+
end
180+
181+
if defined?(::Ractor)
182+
%i[ESC_NO_HL ESC_NO_COLOR ESC_COLORS].each do |name|
183+
test "ResponseParseError::#{name} is Ractor shareable" do
184+
value = Net::IMAP::ResponseParseError.const_get(name)
185+
assert Ractor.shareable? value
186+
end
187+
end
153188
end
154189

155190
test "ResponseTooLargeError" do

0 commit comments

Comments
 (0)