Skip to content

Commit 861886c

Browse files
committed
Be smarter about highlighting first line of failure message
If a failure message has paragraphs or an indented section, only highlight the first part of the message in red instead of just the very first part. This is most applicable to shoulda-matchers, where it is very common to have failure messages like: Expected Foo to have bar, but this could not be proven. Foo did not have a bar. It's also possible however to have messages like: Expected Foo to have bar, but this could not be proven. Foo did not have a bar.
1 parent 7ad6ef6 commit 861886c

File tree

6 files changed

+521
-103
lines changed

6 files changed

+521
-103
lines changed

lib/super_diff/csi.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ def self.decolorize(text)
3636
text.gsub(/\e\[\d+(?:;\d+)*m(.+?)\e\[0m/, '\1')
3737
end
3838

39+
def self.already_colorized?(text)
40+
text.match?(/\e\[\d+m/)
41+
end
42+
3943
def self.inspect_colors_in(text)
4044
[FourBitColor, EightBitColor, TwentyFourBitColor].
4145
reduce(text) do |str, klass|

lib/super_diff/rspec/monkey_patches.rb

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -93,19 +93,31 @@ def initialize(exception, example, options={})
9393
if options.include?(:failure_lines)
9494
@failure_line_groups = {
9595
lines: options[:failure_lines],
96-
already_colored: false
96+
already_colorized: false
9797
}
9898
end
9999
end
100100

101101
# Override to only color uncolored lines in red
102-
def colorized_message_lines(colorizer=::RSpec::Core::Formatters::ConsoleCodes)
102+
# and to not color empty lines
103+
def colorized_message_lines(colorizer = ::RSpec::Core::Formatters::ConsoleCodes)
103104
lines = failure_line_groups.flat_map do |group|
104-
if group[:already_colored]
105+
if group[:already_colorized]
105106
group[:lines]
106107
else
107108
group[:lines].map do |line|
108-
colorizer.wrap(line, message_color)
109+
if line.strip.empty?
110+
line
111+
else
112+
indentation = line[/^[ ]+/]
113+
rest = colorizer.wrap(line.sub(/^[ ]+/, ''), message_color)
114+
115+
if indentation
116+
indentation + rest
117+
else
118+
rest
119+
end
120+
end
109121
end
110122
end
111123
end
@@ -129,33 +141,68 @@ def add_shared_group_lines(lines, colorizer)
129141
# Considering that `failure_slash_error_lines` is already colored,
130142
# extract this from the other lines so that they, too, can be colored,
131143
# later
144+
#
145+
# TODO: Refactor this somehow
146+
#
132147
def failure_line_groups
133-
@failure_line_groups ||= [].tap do |groups|
134-
groups << {
135-
lines: failure_slash_error_lines,
136-
already_colored: true
137-
}
148+
if defined?(@failure_line_groups)
149+
@failure_line_groups
150+
else
151+
@failure_line_groups = [
152+
{
153+
lines: failure_slash_error_lines,
154+
already_colorized: true
155+
}
156+
]
138157

139158
sections = [failure_slash_error_lines, exception_lines]
140159
separate_groups = (
141160
sections.any? { |section| section.size > 1 } &&
142161
!exception_lines.first.empty?
143162
)
163+
144164
if separate_groups
145-
groups << { lines: [''], already_colored: true }
165+
@failure_line_groups << { lines: [''], already_colorized: true }
146166
end
147-
already_has_coloration = exception_lines.any? do |line|
148-
line.match?(/\e\[\d+m/)
167+
168+
already_colorized = exception_lines.any? do |line|
169+
SuperDiff::Csi.already_colorized?(line)
149170
end
150171

151-
groups << {
152-
lines: exception_lines[0..0],
153-
already_colored: already_has_coloration
154-
}
155-
groups << {
156-
lines: exception_lines[1..-1] + extra_failure_lines,
157-
already_colored: true
158-
}
172+
if already_colorized
173+
@failure_line_groups << {
174+
lines: exception_lines,
175+
already_colorized: true
176+
}
177+
else
178+
locatable_exception_lines =
179+
exception_lines.each_with_index.map do |line, index|
180+
{ text: line, index: index }
181+
end
182+
183+
boundary_line =
184+
locatable_exception_lines.find do |line, index|
185+
line[:text].strip.empty? || line[:text].match?(/^ /)
186+
end
187+
188+
if boundary_line
189+
@failure_line_groups << {
190+
lines: exception_lines[0..boundary_line[:index] - 1],
191+
already_colorized: false
192+
}
193+
@failure_line_groups << {
194+
lines: exception_lines[boundary_line[:index]..-1],
195+
already_colorized: true
196+
}
197+
else
198+
@failure_line_groups << {
199+
lines: exception_lines,
200+
already_colorized: false
201+
}
202+
end
203+
end
204+
205+
@failure_line_groups
159206
end
160207
end
161208

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
require "spec_helper"
2+
3+
RSpec.describe "Integration with a third-party matcher", type: :integration do
4+
context "when the matcher is used in the positive and fails" do
5+
context "when the failure message spans multiple lines" do
6+
context "and some of the message is indented" do
7+
it "colorizes the non-indented part in red" do
8+
as_both_colored_and_uncolored do |color_enabled|
9+
snippet = <<~TEST.strip
10+
expect(:anything).to fail_with_indented_multiline_failure_message
11+
TEST
12+
program = make_plain_test_program(
13+
snippet,
14+
color_enabled: color_enabled,
15+
)
16+
17+
expected_output = build_expected_output(
18+
color_enabled: color_enabled,
19+
snippet: %|expect(:anything).to fail_with_indented_multiline_failure_message|,
20+
newline_before_expectation: true,
21+
expectation: proc {
22+
red_line "This is a message that spans multiple lines."
23+
red_line "Here is the next line."
24+
plain_line " This part is indented, for whatever reason. It just kinda keeps"
25+
plain_line " going until we finish saying whatever it is we want to say."
26+
}
27+
)
28+
29+
expect(program).
30+
to produce_output_when_run(expected_output).
31+
in_color(color_enabled)
32+
end
33+
end
34+
end
35+
36+
context "and some of the message is not indented" do
37+
context "and the message is divided into paragraphs" do
38+
it "colorizes the first paragraph in red" do
39+
as_both_colored_and_uncolored do |color_enabled|
40+
snippet = <<~TEST.strip
41+
expect(:anything).to fail_with_paragraphed_failure_message
42+
TEST
43+
program = make_plain_test_program(
44+
snippet,
45+
color_enabled: color_enabled,
46+
)
47+
48+
expected_output = build_expected_output(
49+
color_enabled: color_enabled,
50+
snippet: %|expect(:anything).to fail_with_paragraphed_failure_message|,
51+
newline_before_expectation: true,
52+
expectation: proc {
53+
red_line "This is a message that spans multiple paragraphs."
54+
newline
55+
plain_line "Here is the next paragraph."
56+
}
57+
)
58+
59+
expect(program).
60+
to produce_output_when_run(expected_output).
61+
in_color(color_enabled)
62+
end
63+
end
64+
end
65+
66+
context "and the message is not divided into paragraphs" do
67+
it "colorizes all of the message in red" do
68+
as_both_colored_and_uncolored do |color_enabled|
69+
snippet = <<~TEST.strip
70+
expect(:anything).to fail_with_non_indented_multiline_failure_message
71+
TEST
72+
program = make_plain_test_program(
73+
snippet,
74+
color_enabled: color_enabled,
75+
)
76+
77+
expected_output = build_expected_output(
78+
color_enabled: color_enabled,
79+
snippet: %|expect(:anything).to fail_with_non_indented_multiline_failure_message|,
80+
newline_before_expectation: true,
81+
expectation: proc {
82+
red_line "This is a message that spans multiple lines."
83+
red_line "Here is the next line."
84+
}
85+
)
86+
87+
expect(program).
88+
to produce_output_when_run(expected_output).
89+
in_color(color_enabled)
90+
end
91+
end
92+
end
93+
end
94+
end
95+
96+
context "when the failure message does not span multiple lines" do
97+
it "colorizes all of the message in red" do
98+
as_both_colored_and_uncolored do |color_enabled|
99+
snippet = <<~TEST.strip
100+
expect(:anything).to fail_with_singleline_failure_message
101+
TEST
102+
program = make_plain_test_program(
103+
snippet,
104+
color_enabled: color_enabled,
105+
)
106+
107+
expected_output = build_expected_output(
108+
color_enabled: color_enabled,
109+
snippet: %|expect(:anything).to fail_with_singleline_failure_message|,
110+
expectation: proc {
111+
red_line "This is a message that spans only one line."
112+
}
113+
)
114+
115+
expect(program).
116+
to produce_output_when_run(expected_output).
117+
in_color(color_enabled)
118+
end
119+
end
120+
end
121+
end
122+
123+
context "when the matcher is used in the negative and fails" do
124+
context "when the failure message spans multiple lines" do
125+
context "and some of the message is indented" do
126+
it "colorizes the non-indented part in red" do
127+
as_both_colored_and_uncolored do |color_enabled|
128+
snippet = <<~TEST.strip
129+
expect(:anything).not_to pass_with_indented_multiline_failure_message
130+
TEST
131+
program = make_plain_test_program(
132+
snippet,
133+
color_enabled: color_enabled,
134+
)
135+
136+
expected_output = build_expected_output(
137+
color_enabled: color_enabled,
138+
snippet: %|expect(:anything).not_to pass_with_indented_multiline_failure_message|,
139+
newline_before_expectation: true,
140+
expectation: proc {
141+
red_line "This is a message that spans multiple lines."
142+
red_line "Here is the next line."
143+
plain_line " This part is indented, for whatever reason. It just kinda keeps"
144+
plain_line " going until we finish saying whatever it is we want to say."
145+
}
146+
)
147+
148+
expect(program).
149+
to produce_output_when_run(expected_output).
150+
in_color(color_enabled)
151+
end
152+
end
153+
end
154+
155+
context "and some of the message is not indented" do
156+
context "and the message is divided into paragraphs" do
157+
it "colorizes all of the message in red" do
158+
as_both_colored_and_uncolored do |color_enabled|
159+
snippet = <<~TEST.strip
160+
expect(:anything).not_to pass_with_paragraphed_failure_message
161+
TEST
162+
program = make_plain_test_program(
163+
snippet,
164+
color_enabled: color_enabled,
165+
)
166+
167+
expected_output = build_expected_output(
168+
color_enabled: color_enabled,
169+
snippet: %|expect(:anything).not_to pass_with_paragraphed_failure_message|,
170+
newline_before_expectation: true,
171+
expectation: proc {
172+
red_line "This is a message that spans multiple paragraphs."
173+
newline
174+
plain_line "Here is the next paragraph."
175+
}
176+
)
177+
178+
expect(program).
179+
to produce_output_when_run(expected_output).
180+
in_color(color_enabled)
181+
end
182+
end
183+
end
184+
185+
context "and the message is not divided into paragraphs" do
186+
it "colorizes all of the message in red" do
187+
as_both_colored_and_uncolored do |color_enabled|
188+
snippet = <<~TEST.strip
189+
expect(:anything).not_to pass_with_non_indented_multiline_failure_message
190+
TEST
191+
program = make_plain_test_program(
192+
snippet,
193+
color_enabled: color_enabled,
194+
)
195+
196+
expected_output = build_expected_output(
197+
color_enabled: color_enabled,
198+
snippet: %|expect(:anything).not_to pass_with_non_indented_multiline_failure_message|,
199+
newline_before_expectation: true,
200+
expectation: proc {
201+
red_line "This is a message that spans multiple lines."
202+
red_line "Here is the next line."
203+
}
204+
)
205+
206+
expect(program).
207+
to produce_output_when_run(expected_output).
208+
in_color(color_enabled)
209+
end
210+
end
211+
end
212+
end
213+
end
214+
215+
context "when the failure message does not span multiple lines" do
216+
it "colorizes all of the message in red" do
217+
as_both_colored_and_uncolored do |color_enabled|
218+
snippet = <<~TEST.strip
219+
expect(:anything).not_to pass_with_singleline_failure_message
220+
TEST
221+
program = make_plain_test_program(
222+
snippet,
223+
color_enabled: color_enabled,
224+
)
225+
226+
expected_output = build_expected_output(
227+
color_enabled: color_enabled,
228+
snippet: %|expect(:anything).not_to pass_with_singleline_failure_message|,
229+
expectation: proc {
230+
red_line "This is a message that spans only one line."
231+
}
232+
)
233+
234+
expect(program).
235+
to produce_output_when_run(expected_output).
236+
in_color(color_enabled)
237+
end
238+
end
239+
end
240+
end
241+
end

0 commit comments

Comments
 (0)