Skip to content

Commit 148e1b3

Browse files
authored
Support arbitrary IOs in Spec::CLI (#15882)
This continues the effort to make `Spec` more modular and testable. Colorization state is still global and will be addressed in a subsequent change.
1 parent 5660920 commit 148e1b3

File tree

5 files changed

+61
-51
lines changed

5 files changed

+61
-51
lines changed

src/spec.cr

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,14 @@ module Spec
102102
junit_formatter = Spec::JUnitFormatter.file(Path.new(output_path.not_nil!))
103103
add_formatter(junit_formatter)
104104
when "verbose"
105-
override_default_formatter(Spec::VerboseFormatter.new)
105+
override_default_formatter(Spec::VerboseFormatter.new(@stdout))
106106
when "tap"
107-
override_default_formatter(Spec::TAPFormatter.new)
107+
override_default_formatter(Spec::TAPFormatter.new(@stdout))
108108
end
109109
end
110110

111111
def main(args)
112+
# TODO: should not be global state
112113
Colorize.on_tty_only!
113114

114115
begin
@@ -118,12 +119,11 @@ module Spec
118119
end
119120

120121
unless args.empty?
121-
STDERR.puts "Error: unknown argument '#{args.first}'"
122-
exit 1
122+
abort "Error: unknown argument '#{args.first}'"
123123
end
124124

125125
if ENV["SPEC_VERBOSE"]? == "1"
126-
override_default_formatter(Spec::VerboseFormatter.new)
126+
override_default_formatter(Spec::VerboseFormatter.new(@stdout))
127127
end
128128

129129
add_split_filter ENV["SPEC_SPLIT"]?

src/spec/cli.cr

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ module Spec
1717
getter? dry_run = false
1818
getter? list_tags = false
1919

20+
getter stdout : IO
21+
getter stderr : IO
22+
23+
def initialize(@stdout : IO = STDOUT, @stderr : IO = STDERR)
24+
end
25+
2026
def add_location(file, line)
2127
locations = @locations ||= {} of String => Array(Int32)
2228
locations.put_if_absent(File.expand_path(file)) { [] of Int32 } << line
@@ -69,8 +75,7 @@ module Spec
6975
if location =~ /\A(.+?)\:(\d+)\Z/
7076
add_location $1, $2.to_i
7177
else
72-
STDERR.puts "location #{location} must be file:line"
73-
exit 1
78+
abort "location #{location} must be file:line"
7479
end
7580
end
7681
opts.on("--tag TAG", "run examples with the specified TAG, or exclude examples by adding ~ before the TAG.") do |tag|
@@ -92,7 +97,7 @@ module Spec
9297
configure_formatter("junit", output_path)
9398
end
9499
opts.on("-h", "--help", "show this help") do |pattern|
95-
puts opts
100+
@stdout.puts opts
96101
exit
97102
end
98103
opts.on("-v", "--verbose", "verbose output") do
@@ -102,9 +107,11 @@ module Spec
102107
configure_formatter("tap")
103108
end
104109
opts.on("--color", "Enabled ANSI colored output") do
110+
# TODO: should not be global state
105111
Colorize.enabled = true
106112
end
107113
opts.on("--no-color", "Disable ANSI colored output") do
114+
# TODO: should not be global state
108115
Colorize.enabled = false
109116
end
110117
opts.on("--dry-run", "Pass all tests without execution") do
@@ -121,6 +128,11 @@ module Spec
121128
# The real implementation in `../spec.cr` overrides this for actual use.
122129
def configure_formatter(formatter, output_path = nil)
123130
end
131+
132+
private def abort(msg)
133+
@stderr.puts msg
134+
exit 1
135+
end
124136
end
125137

126138
@[Deprecated("This is an internal API.")]

src/spec/context.cr

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ module Spec
150150
end
151151

152152
def run
153-
print_order_message
153+
print_order_message(cli.stdout)
154154

155155
internal_run
156156
end
@@ -174,17 +174,17 @@ module Spec
174174
def finish(elapsed_time, aborted = false)
175175
cli.formatters.each(&.finish(elapsed_time, aborted))
176176
if cli.formatters.any?(&.should_print_summary?)
177-
print_summary(elapsed_time, aborted)
177+
print_summary(cli.stdout, elapsed_time, aborted)
178178
end
179179
end
180180

181-
def print_summary(elapsed_time, aborted = false)
181+
def print_summary(io : IO, elapsed_time, aborted = false)
182182
pendings = results_for(:pending)
183183
unless pendings.empty?
184-
puts
185-
puts "Pending:"
184+
io.puts
185+
io.puts "Pending:"
186186
pendings.each do |pending|
187-
puts Spec.color(" #{pending.description}", :pending)
187+
io.puts Spec.color(" #{pending.description}", :pending)
188188
end
189189
end
190190

@@ -195,50 +195,50 @@ module Spec
195195

196196
failures_and_errors = failures + errors
197197
unless failures_and_errors.empty?
198-
puts
199-
puts "Failures:"
198+
io.puts
199+
io.puts "Failures:"
200200
failures_and_errors.each_with_index do |fail, i|
201201
if ex = fail.exception
202-
puts
203-
puts "#{(i + 1).to_s.rjust(3, ' ')}) #{fail.description}"
202+
io.puts
203+
io.puts "#{(i + 1).to_s.rjust(3, ' ')}) #{fail.description}"
204204

205205
if ex.is_a?(SpecError)
206206
source_line = Spec.read_line(ex.file, ex.line)
207207
if source_line
208-
puts Spec.color(" Failure/Error: #{source_line.strip}", :error)
208+
io.puts Spec.color(" Failure/Error: #{source_line.strip}", :error)
209209
end
210210
end
211-
puts
211+
io.puts
212212

213213
message = ex.is_a?(SpecError) ? ex.to_s : ex.inspect_with_backtrace
214214
message.split('\n') do |line|
215-
print " "
216-
puts Spec.color(line, :error)
215+
io.print " "
216+
io.puts Spec.color(line, :error)
217217
end
218218

219219
if ex.is_a?(SpecError)
220-
puts
221-
puts Spec.color(" # #{Path[ex.file].relative_to(cwd)}:#{ex.line}", :comment)
220+
io.puts
221+
io.puts Spec.color(" # #{Path[ex.file].relative_to(cwd)}:#{ex.line}", :comment)
222222
end
223223
end
224224
end
225225
end
226226

227227
if cli.slowest
228-
puts
228+
io.puts
229229
results = results_for(:success) + results_for(:fail)
230230
top_n = results.sort_by { |res| -res.elapsed.not_nil!.to_f }[0..cli.slowest.not_nil!]
231231
top_n_time = top_n.sum &.elapsed.not_nil!.total_seconds
232232
percent = (top_n_time * 100) / elapsed_time.total_seconds
233-
puts "Top #{cli.slowest} slowest examples (#{top_n_time.humanize} seconds, #{percent.round(2)}% of total time):"
233+
io.puts "Top #{cli.slowest} slowest examples (#{top_n_time.humanize} seconds, #{percent.round(2)}% of total time):"
234234
top_n.each do |res|
235-
puts " #{res.description}"
235+
io.puts " #{res.description}"
236236
res_elapsed = res.elapsed.not_nil!.total_seconds.humanize
237-
puts " #{res_elapsed.colorize.bold} seconds #{Path[res.file].relative_to(cwd)}:#{res.line}"
237+
io.puts " #{res_elapsed.colorize.bold} seconds #{Path[res.file].relative_to(cwd)}:#{res.line}"
238238
end
239239
end
240240

241-
puts
241+
io.puts
242242

243243
success = results_for(:success)
244244
total = pendings.size + failures.size + errors.size + success.size
@@ -250,27 +250,27 @@ module Spec
250250
else Status::Success
251251
end
252252

253-
puts "Aborted!".colorize.red if aborted
254-
puts "Finished in #{Spec.to_human(elapsed_time)}"
255-
puts Spec.color("#{total} examples, #{failures.size} failures, #{errors.size} errors, #{pendings.size} pending", final_status)
256-
puts Spec.color("Only running `focus: true`", :focus) if cli.focus?
253+
io.puts "Aborted!".colorize.red if aborted
254+
io.puts "Finished in #{Spec.to_human(elapsed_time)}"
255+
io.puts Spec.color("#{total} examples, #{failures.size} failures, #{errors.size} errors, #{pendings.size} pending", final_status)
256+
io.puts Spec.color("Only running `focus: true`", :focus) if cli.focus?
257257

258258
unless failures_and_errors.empty?
259-
puts
260-
puts "Failed examples:"
261-
puts
259+
io.puts
260+
io.puts "Failed examples:"
261+
io.puts
262262
failures_and_errors.each do |fail|
263-
print Spec.color("crystal spec #{Path[fail.file].relative_to(cwd)}:#{fail.line}", :error)
264-
puts Spec.color(" # #{fail.description}", :comment)
263+
io.print Spec.color("crystal spec #{Path[fail.file].relative_to(cwd)}:#{fail.line}", :error)
264+
io.puts Spec.color(" # #{fail.description}", :comment)
265265
end
266266
end
267267

268-
print_order_message
268+
print_order_message(io)
269269
end
270270

271-
def print_order_message
271+
def print_order_message(io : IO)
272272
if randomizer_seed = cli.randomizer_seed
273-
puts Spec.color("Randomized with seed: #{randomizer_seed}", :order)
273+
io.puts Spec.color("Randomized with seed: #{randomizer_seed}", :order)
274274
end
275275
end
276276

src/spec/dsl.cr

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,9 @@ module Spec
220220
execute_examples
221221
end
222222
rescue ex
223-
STDERR.print "Unhandled exception: "
224-
ex.inspect_with_backtrace(STDERR)
225-
STDERR.flush
223+
@stderr.print "Unhandled exception: "
224+
ex.inspect_with_backtrace(@stderr)
225+
@stderr.flush
226226
@aborted = true
227227
ensure
228228
finish_run unless list_tags?
@@ -273,7 +273,7 @@ module Spec
273273
return if tag_counts.empty?
274274
longest_name_size = tag_counts.keys.max_of(&.size)
275275
tag_counts.to_a.sort_by! { |k, v| {-v, k} }.each do |tag_name, count|
276-
puts "#{tag_name.rjust(longest_name_size)}: #{count}"
276+
@stdout.puts "#{tag_name.rjust(longest_name_size)}: #{count}"
277277
end
278278
end
279279

src/spec/formatter.cr

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module Spec
22
# :nodoc:
33
abstract class Formatter
4-
def initialize(@io : IO = STDOUT)
4+
def initialize(@io : IO)
55
end
66

77
def push(context)
@@ -118,18 +118,16 @@ module Spec
118118

119119
# :nodoc:
120120
class CLI
121-
@formatters = [Spec::DotFormatter.new] of Spec::Formatter
122-
123121
def formatters
124-
@formatters
122+
@formatters ||= [Spec::DotFormatter.new(@stdout)] of Spec::Formatter
125123
end
126124

127125
def override_default_formatter(formatter)
128-
@formatters[0] = formatter
126+
formatters[0] = formatter
129127
end
130128

131129
def add_formatter(formatter)
132-
@formatters << formatter
130+
formatters << formatter
133131
end
134132
end
135133

0 commit comments

Comments
 (0)