Skip to content
This repository was archived by the owner on Nov 30, 2024. It is now read-only.

Commit 7d558b8

Browse files
committed
Implement syntax highlighting.
1 parent 19761e3 commit 7d558b8

File tree

12 files changed

+252
-7
lines changed

12 files changed

+252
-7
lines changed

Changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ Enhancements:
5252
* Enhance `fail_fast` option so it can take a number (e.g. `--fail-fast=3`)
5353
to force the run to abort after the specified number of failures.
5454
(Jack Scotti, #2065)
55+
* Syntax highlight the failure snippets in text formatters when `color`
56+
is enabled and the `coderay` gem is installed on a POSIX system.
57+
(Myron Marston, #2109)
5558

5659
Bug Fixes:
5760

lib/rspec/core/formatters/exception_presenter.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def failure_slash_error_lines
159159
if lines.count == 1
160160
lines[0] = "Failure/Error: #{lines[0].strip}"
161161
else
162-
least_indentation = lines.map { |line| line[/^[ \t]*/] }.min
162+
least_indentation = SnippetExtractor.least_indentation_from(lines)
163163
lines = lines.map { |line| line.sub(/^#{least_indentation}/, ' ') }
164164
lines.unshift('Failure/Error:')
165165
end
@@ -204,7 +204,8 @@ def read_failed_lines
204204

205205
file_path, line_number = matching_line.match(/(.+?):(\d+)(|:\d+)/)[1..2]
206206
max_line_count = RSpec.configuration.max_displayed_failure_line_count
207-
SnippetExtractor.extract_expression_lines_at(file_path, line_number.to_i, max_line_count)
207+
lines = SnippetExtractor.extract_expression_lines_at(file_path, line_number.to_i, max_line_count)
208+
RSpec.world.source_cache.syntax_highlighter.highlight(lines)
208209
rescue SnippetExtractor::NoSuchFileError
209210
["Unable to find #{file_path} to read failed line"]
210211
rescue SnippetExtractor::NoSuchLineError

lib/rspec/core/formatters/snippet_extractor.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ def self.extract_expression_lines_at(file_path, beginning_line_number, *)
133133
end
134134
# :nocov:
135135
end
136+
137+
def self.least_indentation_from(lines)
138+
lines.map { |line| line[/^[ \t]*/] }.min
139+
end
136140
end
137141
end
138142
end

lib/rspec/core/notifications.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,9 @@ def fully_formatted
281281
# @attr pending_examples [Array<RSpec::Core::Example>] the pending examples
282282
# @attr load_time [Float] the number of seconds taken to boot RSpec
283283
# and load the spec files
284-
SummaryNotification = Struct.new(:duration, :examples, :failed_examples, :pending_examples, :load_time)
284+
# @attr syntax_highlighting_unavailable [Boolean] indicates if syntax highlighting
285+
# was attempted to be used but was unavailable.
286+
SummaryNotification = Struct.new(:duration, :examples, :failed_examples, :pending_examples, :load_time, :syntax_highlighting_unavailable)
285287
class SummaryNotification
286288
# @api
287289
# @return [Fixnum] the number of examples run
@@ -359,7 +361,11 @@ def formatted_load_time
359361
# @return [String] The summary information fully formatted in the way that
360362
# RSpec's built-in formatters emit.
361363
def fully_formatted(colorizer=::RSpec::Core::Formatters::ConsoleCodes)
362-
formatted = "\nFinished in #{formatted_duration} " \
364+
formatted = ""
365+
if syntax_highlighting_unavailable
366+
formatted << "\nSyntax highlighting of failure snippets unavailable -- install the coderay gem to enable it.\n"
367+
end
368+
formatted << "\nFinished in #{formatted_duration} " \
363369
"(files took #{formatted_load_time} to load)\n" \
364370
"#{colorized_totals_line(colorizer)}\n"
365371

lib/rspec/core/reporter.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ def initialize(configuration)
2222

2323
# @private
2424
attr_reader :examples, :failed_examples, :pending_examples
25+
# @private
26+
attr_accessor :syntax_highlighting_unavailable
2527

2628
# @private
2729
def reset
@@ -165,7 +167,8 @@ def finish
165167
@profiler.example_groups)
166168
end
167169
notify :dump_summary, Notifications::SummaryNotification.new(@duration, @examples, @failed_examples,
168-
@pending_examples, @load_time)
170+
@pending_examples, @load_time,
171+
syntax_highlighting_unavailable)
169172
notify :seed, Notifications::SeedNotification.new(@configuration.seed, seed_used?)
170173
end
171174
end

lib/rspec/core/source.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
RSpec::Support.require_rspec_core 'source/node'
2+
RSpec::Support.require_rspec_core 'source/syntax_highlighter'
23
RSpec::Support.require_rspec_core 'source/token'
34

45
module RSpec
@@ -59,8 +60,11 @@ def inspect
5960

6061
# @private
6162
class Cache
62-
def initialize
63+
attr_reader :syntax_highlighter
64+
65+
def initialize(configuration)
6366
@sources_by_path = {}
67+
@syntax_highlighter = SyntaxHighlighter.new(configuration)
6468
end
6569

6670
def source_from_file(path)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
module RSpec
2+
module Core
3+
class Source
4+
# @private
5+
# Provides terminal syntax highlighting of code snippets
6+
# when coderay is available.
7+
class SyntaxHighlighter
8+
def initialize(configuration)
9+
@configuration = configuration
10+
end
11+
12+
def highlight(lines)
13+
implementation.highlight_syntax(lines)
14+
end
15+
16+
private
17+
18+
if RSpec::Support::OS.windows?
19+
# :nocov:
20+
def implementation
21+
WindowsImplementation
22+
end
23+
# :nocov:
24+
else
25+
def implementation
26+
return color_enabled_implementation if @configuration.color_enabled?
27+
NoSyntaxHighlightingImplementation
28+
end
29+
end
30+
31+
def color_enabled_implementation
32+
@color_enabled_implementation ||= begin
33+
::Kernel.require 'coderay'
34+
@configuration.reporter.syntax_highlighting_unavailable = false
35+
CodeRayImplementation
36+
rescue LoadError
37+
@configuration.reporter.syntax_highlighting_unavailable = true
38+
NoSyntaxHighlightingImplementation
39+
end
40+
end
41+
42+
# @private
43+
module CodeRayImplementation
44+
RESET_CODE = "\e[0m"
45+
46+
def self.highlight_syntax(lines)
47+
highlighted = CodeRay.encode(lines.join("\n"), :ruby, :terminal)
48+
highlighted.split("\n").map do |line|
49+
line.sub(/\S/) { |char| char.insert(0, RESET_CODE) }
50+
end
51+
end
52+
end
53+
54+
# @private
55+
module NoSyntaxHighlightingImplementation
56+
def self.highlight_syntax(lines)
57+
lines
58+
end
59+
end
60+
61+
# @private
62+
# Not sure why, but our code above (and/or coderay itself) does not work
63+
# on Windows, so we disable the feature on Windows.
64+
WindowsImplementation = NoSyntaxHighlightingImplementation
65+
end
66+
end
67+
end
68+
end

lib/rspec/core/world.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def reporter
111111
def source_cache
112112
@source_cache ||= begin
113113
RSpec::Support.require_rspec_core "source"
114-
Source::Cache.new
114+
Source::Cache.new(@configuration)
115115
end
116116
end
117117

spec/rspec/core/formatters/exception_presenter_spec.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,24 @@ module RSpec::Core
187187
end
188188
end
189189

190+
describe "syntax highlighting" do
191+
let(:expression) do
192+
expect('RSpec').to be_a(Integer)
193+
end
194+
195+
it 'uses our syntax highlighter on the code snippet to format it nicely' do
196+
syntax_highlighter = instance_double(Source::SyntaxHighlighter)
197+
allow(syntax_highlighter).to receive(:highlight) do |lines|
198+
lines.map { |l| "<highlighted>#{l.strip}</highlighted>" }
199+
end
200+
201+
allow(RSpec.world.source_cache).to receive_messages(:syntax_highlighter => syntax_highlighter)
202+
203+
formatted = presenter.fully_formatted(1)
204+
expect(formatted).to include("<highlighted>expect('RSpec').to be_a(Integer)</highlighted>")
205+
end
206+
end
207+
190208
context 'with single line expression and single line RSpec exception message' do
191209
let(:expression) do
192210
expect('RSpec').to be_a(Integer)

spec/rspec/core/reporter_spec.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,34 @@ module RSpec::Core
3434

3535
reporter.finish
3636
end
37+
38+
let(:install_coderay_snippet) { "install the coderay gem" }
39+
40+
def formatter_notified_of_dump_summary(syntax_highlighting_unavailable)
41+
formatter = spy("formatter")
42+
reporter.syntax_highlighting_unavailable = syntax_highlighting_unavailable
43+
reporter.register_listener formatter, :dump_summary
44+
45+
reporter.start(0)
46+
reporter.finish
47+
formatter
48+
end
49+
50+
it "includes a note about install coderay if syntax highlighting is unavailable" do
51+
formatter = formatter_notified_of_dump_summary(true)
52+
53+
expect(formatter).to have_received(:dump_summary).with(an_object_having_attributes(
54+
:fully_formatted => a_string_including(install_coderay_snippet)
55+
))
56+
end
57+
58+
it "does not include a note about installing coderay if syntax highlighting is available" do
59+
formatter = formatter_notified_of_dump_summary(false)
60+
61+
expect(formatter).to have_received(:dump_summary).with(an_object_having_attributes(
62+
:fully_formatted => a_string_excluding(install_coderay_snippet)
63+
))
64+
end
3765
end
3866

3967
describe 'start' do
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
require 'rspec/core/source/syntax_highlighter'
2+
3+
class RSpec::Core::Source
4+
RSpec.describe SyntaxHighlighter do
5+
let(:config) { RSpec::Core::Configuration.new.tap { |c| c.color = true } }
6+
let(:highlighter) { SyntaxHighlighter.new(config) }
7+
8+
def be_highlighted
9+
include("\e[32m")
10+
end
11+
12+
context "when CodeRay is available", :unless => RSpec::Support::OS.windows? do
13+
before { expect { require 'coderay' }.not_to raise_error }
14+
15+
it 'highlights the syntax of the provided lines' do
16+
highlighted = highlighter.highlight(['[:ok, "ok"]'])
17+
expect(highlighted.size).to eq(1)
18+
expect(highlighted.first).to be_highlighted.and include(":ok")
19+
end
20+
21+
it 'prefixes the each line with a reset escape code so it can be interpolated in a colored string without affecting the syntax highlighting of the snippet' do
22+
highlighted = highlighter.highlight(['a = 1', 'b = 2'])
23+
expect(highlighted).to all start_with("\e[0m")
24+
end
25+
26+
it 'leaves leading spaces alone so it can be re-indented as needed without the leading reset code interfering' do
27+
highlighted = highlighter.highlight([' a = 1', ' b = 2'])
28+
expect(highlighted).to all start_with(" \e[0m")
29+
end
30+
31+
it 'returns the provided lines unmodified if color is disabled' do
32+
config.color = false
33+
expect(highlighter.highlight(['[:ok, "ok"]'])).to eq(['[:ok, "ok"]'])
34+
end
35+
36+
it 'dynamically adjusts to changing color config' do
37+
config.color = false
38+
expect(highlighter.highlight(['[:ok, "ok"]']).first).not_to be_highlighted
39+
config.color = true
40+
expect(highlighter.highlight(['[:ok, "ok"]']).first).to be_highlighted
41+
config.color = false
42+
expect(highlighter.highlight(['[:ok, "ok"]']).first).not_to be_highlighted
43+
end
44+
45+
it 'notifies the reporter' do
46+
config.reporter.syntax_highlighting_unavailable = true
47+
48+
expect {
49+
highlighter.highlight([""])
50+
}.to change { config.reporter.syntax_highlighting_unavailable }.to(false)
51+
end
52+
53+
it 'does not notify the reporter if highlighting is never attempted' do
54+
config.reporter.syntax_highlighting_unavailable = true
55+
56+
expect {
57+
SyntaxHighlighter.new(config)
58+
}.not_to change { config.reporter.syntax_highlighting_unavailable }
59+
end
60+
end
61+
62+
context "when CodeRay is unavailable" do
63+
before do
64+
allow(::Kernel).to receive(:require).with("coderay").and_raise(LoadError)
65+
end
66+
67+
it 'does not highlight the syntax' do
68+
unhighlighted = highlighter.highlight(['[:ok, "ok"]'])
69+
expect(unhighlighted.size).to eq(1)
70+
expect(unhighlighted.first).not_to be_highlighted
71+
end
72+
73+
it 'does not mutate the input array' do
74+
lines = ["a = 1", "b = 2"]
75+
expect { highlighter.highlight(lines) }.not_to change { lines }
76+
end
77+
78+
it 'does not add the comment about coderay if the snippet is only one line as we do not want to convert it to multiline just for the comment' do
79+
expect(highlighter.highlight(["a = 1"])).to eq(["a = 1"])
80+
end
81+
82+
it 'does not add the comment about coderay if given no lines' do
83+
expect(highlighter.highlight([])).to eq([])
84+
end
85+
86+
it 'does not add the comment about coderay if color id disabled even when given a multiline snippet' do
87+
config.color = false
88+
lines = ["a = 1", "b = 2"]
89+
expect(highlighter.highlight(lines)).to eq(lines)
90+
end
91+
92+
it 'notifies the reporter' do
93+
config.reporter.syntax_highlighting_unavailable = false
94+
95+
expect {
96+
highlighter.highlight([""])
97+
}.to change { config.reporter.syntax_highlighting_unavailable }.to(true)
98+
end
99+
100+
it 'does not notify the reporter if highlighting is never attempted' do
101+
config.reporter.syntax_highlighting_unavailable = false
102+
103+
expect {
104+
SyntaxHighlighter.new(config)
105+
}.not_to change { config.reporter.syntax_highlighting_unavailable }
106+
end
107+
end
108+
end
109+
end

spec/support/matchers.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,5 +122,6 @@ def failure_reason(example)
122122
RSpec::Matchers.define_negated_matcher :avoid_outputting, :output
123123
RSpec::Matchers.define_negated_matcher :exclude, :include
124124
RSpec::Matchers.define_negated_matcher :excluding, :include
125+
RSpec::Matchers.define_negated_matcher :a_string_excluding, :a_string_including
125126
RSpec::Matchers.define_negated_matcher :avoid_changing, :change
126127
RSpec::Matchers.define_negated_matcher :a_hash_excluding, :include

0 commit comments

Comments
 (0)