loupe 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +71 -0
- data/README.md +101 -0
- data/Rakefile +16 -0
- data/exe/loupe +9 -0
- data/lib/loupe/cli.rb +83 -0
- data/lib/loupe/color.rb +31 -0
- data/lib/loupe/executor.rb +46 -0
- data/lib/loupe/expectation.rb +538 -0
- data/lib/loupe/failure.rb +49 -0
- data/lib/loupe/paged_reporter.rb +195 -0
- data/lib/loupe/plain_reporter.rb +27 -0
- data/lib/loupe/process_executor.rb +56 -0
- data/lib/loupe/queue_server.rb +49 -0
- data/lib/loupe/ractor_executor.rb +52 -0
- data/lib/loupe/rake_task.rb +47 -0
- data/lib/loupe/reporter.rb +106 -0
- data/lib/loupe/test.rb +270 -0
- data/lib/loupe/version.rb +6 -0
- data/lib/loupe.rb +18 -0
- metadata +95 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/console"
|
|
4
|
+
|
|
5
|
+
module Loupe
|
|
6
|
+
# Pager
|
|
7
|
+
#
|
|
8
|
+
# This class is responsible for paginating the test failures,
|
|
9
|
+
# and implementing an interface for interacting with them.
|
|
10
|
+
class PagedReporter < Reporter # rubocop:disable Metrics/ClassLength
|
|
11
|
+
# @return [void]
|
|
12
|
+
def print_summary
|
|
13
|
+
@current_page = 0
|
|
14
|
+
@console = IO.console
|
|
15
|
+
@runtime = Time.now - @start_time
|
|
16
|
+
@running = true
|
|
17
|
+
page
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# Main loop of the pager
|
|
23
|
+
# Allow users to navigate through pages of test failures
|
|
24
|
+
# and interact with them.
|
|
25
|
+
# @return [void]
|
|
26
|
+
def page
|
|
27
|
+
while @running
|
|
28
|
+
@current_failure = @failures[@current_page]
|
|
29
|
+
@mid_width = @console.winsize[1] / 2
|
|
30
|
+
header
|
|
31
|
+
|
|
32
|
+
if @failures.empty?
|
|
33
|
+
puts "All tests fixed"
|
|
34
|
+
@running = false
|
|
35
|
+
else
|
|
36
|
+
file_preview
|
|
37
|
+
menu
|
|
38
|
+
handle_raw_command
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Read a raw command from the console and match it
|
|
44
|
+
#
|
|
45
|
+
# @return [void]
|
|
46
|
+
def handle_raw_command # rubocop:disable Metrics/CyclomaticComplexity
|
|
47
|
+
case @console.raw { |c| c.read(1) }
|
|
48
|
+
when "j"
|
|
49
|
+
@current_page += 1 unless @current_page == @failures.length - 1
|
|
50
|
+
when "k"
|
|
51
|
+
@current_page -= 1 unless @current_page.zero?
|
|
52
|
+
when "o"
|
|
53
|
+
open_editor
|
|
54
|
+
when "f"
|
|
55
|
+
@failures.delete_at(@current_page)
|
|
56
|
+
@failure_count -= 1
|
|
57
|
+
@success_count += 1
|
|
58
|
+
@current_page -= 1 unless @current_page.zero?
|
|
59
|
+
when "r"
|
|
60
|
+
rerun_failure
|
|
61
|
+
when "q"
|
|
62
|
+
@running = false
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Print the summary at the top of the screen.
|
|
67
|
+
# This string has to be updated every time, since the statistics
|
|
68
|
+
# might change if the user has marked tests as fixed
|
|
69
|
+
# return [String]
|
|
70
|
+
def summary
|
|
71
|
+
<<~SUMMARY
|
|
72
|
+
Tests: #{@test_count} Expectations: #{@expectation_count}
|
|
73
|
+
Passed: #{@success_count} Failures: #{@failure_count}
|
|
74
|
+
|
|
75
|
+
Finished in #{@runtime} seconds
|
|
76
|
+
SUMMARY
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Prints a bar at the top with the summary of the test run
|
|
80
|
+
# including totals failures and expectations
|
|
81
|
+
# @return [void]
|
|
82
|
+
def header
|
|
83
|
+
@console.erase_screen(2)
|
|
84
|
+
@console.cursor = [0, 0]
|
|
85
|
+
bar = "=" * @console.winsize[1]
|
|
86
|
+
puts "#{bar}\n#{summary}\n#{bar}\n"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Print the preview of the file where a failure occurred
|
|
90
|
+
# add a line indicating where exactly it broke
|
|
91
|
+
# @return [void]
|
|
92
|
+
def file_preview
|
|
93
|
+
lines = File.readlines(@current_failure.file_name)
|
|
94
|
+
|
|
95
|
+
lines.insert(
|
|
96
|
+
@current_failure.line_number + 1,
|
|
97
|
+
"#{indentation_on_failure_line(lines)}^^^ #{@current_failure.message.gsub(/(\[\d;\d{2}m|\[0m)/, '')}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
content = lines[@current_failure.line_number - 5..@current_failure.line_number + 5].join("\n")
|
|
101
|
+
puts content
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The indentation on the line where the failure happened
|
|
105
|
+
# so that the error message can be inserted at the right level
|
|
106
|
+
# @param lines [Array<String>]
|
|
107
|
+
# return [String]
|
|
108
|
+
def indentation_on_failure_line(lines)
|
|
109
|
+
" " * (lines[@current_failure.line_number].chars.index { |c| c != " " })
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [void]
|
|
113
|
+
def menu
|
|
114
|
+
location, message = @current_failure.location_and_message
|
|
115
|
+
|
|
116
|
+
print_on_right_side(7, @status)
|
|
117
|
+
print_on_right_side(9, location)
|
|
118
|
+
print_on_right_side(10, message)
|
|
119
|
+
|
|
120
|
+
print_on_right_side(12, "Commands")
|
|
121
|
+
print_on_right_side(14, "j (next)")
|
|
122
|
+
print_on_right_side(15, "k (previous)")
|
|
123
|
+
print_on_right_side(16, "o (open in editor)")
|
|
124
|
+
print_on_right_side(17, "f (mark as fixed)")
|
|
125
|
+
print_on_right_side(18, "r (rerun selected test)")
|
|
126
|
+
print_on_right_side(19, "q (quit)")
|
|
127
|
+
|
|
128
|
+
@status = nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The first half of the screen is the file preview.
|
|
132
|
+
# This helper method assists in printing things on the
|
|
133
|
+
# other side of the screen.
|
|
134
|
+
#
|
|
135
|
+
# Always clear coloring afterwards
|
|
136
|
+
#
|
|
137
|
+
# return [void]
|
|
138
|
+
def print_on_right_side(row, message)
|
|
139
|
+
@console.cursor = [row, @mid_width + 1]
|
|
140
|
+
available_length = @console.winsize[1] - @mid_width + 1
|
|
141
|
+
print message.to_s[0, available_length]
|
|
142
|
+
print "\033[0m"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Open the editor selected by options (or defined by $EDITOR) with the current
|
|
146
|
+
# failure being viewed.
|
|
147
|
+
# @return [void]
|
|
148
|
+
def open_editor
|
|
149
|
+
editor = @options[:editor] || ENV["EDITOR"]
|
|
150
|
+
executable = editor_executable(editor)
|
|
151
|
+
|
|
152
|
+
case editor
|
|
153
|
+
when "vim", "nvim"
|
|
154
|
+
spawn "#{executable} +#{@current_failure.line_number} #{@current_failure.file_name}"
|
|
155
|
+
when "code"
|
|
156
|
+
spawn "#{executable} -g #{@current_failure.file_name}:#{@current_failure.line_number}"
|
|
157
|
+
else
|
|
158
|
+
spawn "#{executable} #{@current_failure.file_name}"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Attempt to find the editor's executable within
|
|
163
|
+
# the given PATHs
|
|
164
|
+
# @param editor [String]
|
|
165
|
+
# @return [String]
|
|
166
|
+
def editor_executable(editor)
|
|
167
|
+
ENV["PATH"].split(":").each do |p|
|
|
168
|
+
path = File.join(p, editor)
|
|
169
|
+
return path if File.exist?(path)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Rerun the current failure
|
|
174
|
+
#
|
|
175
|
+
# Since the developer is changing the test file to fix it,
|
|
176
|
+
# we need to unload it from LOADED_FEATURES and require it again.
|
|
177
|
+
# Otherwise, we would just be re-running the same test loaded in memory
|
|
178
|
+
# and it would never pass.
|
|
179
|
+
#
|
|
180
|
+
# @return void
|
|
181
|
+
def rerun_failure
|
|
182
|
+
$LOADED_FEATURES.delete(@current_failure.file_name)
|
|
183
|
+
require @current_failure.file_name
|
|
184
|
+
|
|
185
|
+
reporter = @current_failure.klass.run(@current_failure.test_name, @options)
|
|
186
|
+
|
|
187
|
+
if reporter.failures.empty?
|
|
188
|
+
@status = "#{@color.p('Fixed', :green)}. Click f to remove from list"
|
|
189
|
+
else
|
|
190
|
+
@failures[@current_page] = reporter.failures.first
|
|
191
|
+
@status = @color.p("Still failing", :red)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Loupe
|
|
4
|
+
# PlainReporter
|
|
5
|
+
#
|
|
6
|
+
# A simple reporter that just prints dots and Fs to
|
|
7
|
+
# the terminal
|
|
8
|
+
class PlainReporter < Reporter
|
|
9
|
+
# @return [void]
|
|
10
|
+
def print_summary
|
|
11
|
+
if @failures.empty?
|
|
12
|
+
report = ""
|
|
13
|
+
else
|
|
14
|
+
report = +"\n\n"
|
|
15
|
+
report << @failures.map!(&:to_s).join("\n")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
print "\n\n"
|
|
19
|
+
print <<~SUMMARY
|
|
20
|
+
Tests: #{@test_count} Expectations: #{@expectation_count}
|
|
21
|
+
Passed: #{@success_count} Failures: #{@failure_count}#{report}
|
|
22
|
+
|
|
23
|
+
Finished in #{Time.now - @start_time} seconds
|
|
24
|
+
SUMMARY
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "drb/drb"
|
|
4
|
+
|
|
5
|
+
module Loupe
|
|
6
|
+
# ProcessExecutor
|
|
7
|
+
#
|
|
8
|
+
# This class is responsible for executing tests in process mode.
|
|
9
|
+
class ProcessExecutor < Executor
|
|
10
|
+
# Create a new ProcessExecutor
|
|
11
|
+
#
|
|
12
|
+
# This will create a new server object that will be shared
|
|
13
|
+
# with child processes using DRb
|
|
14
|
+
# @param options [Hash<Symbol, BasicObject>]
|
|
15
|
+
# @return [Loupe::Executor]
|
|
16
|
+
def initialize(options)
|
|
17
|
+
super
|
|
18
|
+
|
|
19
|
+
@server = QueueServer.new(populate_queue, @reporter)
|
|
20
|
+
@url = DRb.start_service("drbunix:", @server).uri
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# run
|
|
24
|
+
#
|
|
25
|
+
# Fork each one of the process workers and connect with the server
|
|
26
|
+
# object coming from DRb. Run until the queue is clear
|
|
27
|
+
# @return [Integer]
|
|
28
|
+
def run
|
|
29
|
+
@workers = (0...[Etc.nprocessors, @server.length].min).map do
|
|
30
|
+
fork do
|
|
31
|
+
DRb.start_service
|
|
32
|
+
server = DRbObject.new_with_uri(@url)
|
|
33
|
+
|
|
34
|
+
until server.empty?
|
|
35
|
+
klass, method_name = server.pop
|
|
36
|
+
server.add_reporter(klass.run(method_name, @options)) if klass && method_name
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
shutdown
|
|
42
|
+
@reporter.print_summary
|
|
43
|
+
@reporter.exit_status
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# Wait until all child processes finish executing tests
|
|
49
|
+
# and then stop the DRb service
|
|
50
|
+
# return [void]
|
|
51
|
+
def shutdown
|
|
52
|
+
@workers.each { |pid| Process.waitpid(pid) }
|
|
53
|
+
DRb.stop_service
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Loupe
|
|
4
|
+
# Server
|
|
5
|
+
#
|
|
6
|
+
# This object is the one passed to DRb in order to
|
|
7
|
+
# communicate between worker and server processes and coordinate
|
|
8
|
+
# both the queue and the reporting results
|
|
9
|
+
class QueueServer
|
|
10
|
+
# The two operations we need to synchronize between the
|
|
11
|
+
# main process and its children is the queue and the reporter.
|
|
12
|
+
# We need to share the queue, so that workers can pop the tests from it
|
|
13
|
+
# and we need to share the reporter, so that workers can update the results
|
|
14
|
+
#
|
|
15
|
+
# @param queue [Array<Array<Class, Symbol>>]
|
|
16
|
+
# @param reporter [Loupe::Reporter]
|
|
17
|
+
# @return [Loupe::Server]
|
|
18
|
+
def initialize(queue, reporter)
|
|
19
|
+
@queue = queue
|
|
20
|
+
@reporter = reporter
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# add_reporter
|
|
24
|
+
#
|
|
25
|
+
# Adds a temporary reporter from a child process into
|
|
26
|
+
# the main reporter to aggregate results
|
|
27
|
+
#
|
|
28
|
+
# @param other [Loupe::Reporter]
|
|
29
|
+
# @return [void]
|
|
30
|
+
def add_reporter(other)
|
|
31
|
+
@reporter << other
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [Array<Class, Symbol>]
|
|
35
|
+
def pop
|
|
36
|
+
@queue.pop
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Integer]
|
|
40
|
+
def length
|
|
41
|
+
@queue.length
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Boolean]
|
|
45
|
+
def empty?
|
|
46
|
+
@queue.empty?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Loupe
|
|
4
|
+
# RactorExecutor
|
|
5
|
+
#
|
|
6
|
+
# This class is responsible for the execution flow. It populates
|
|
7
|
+
# the queue of tests to be executed, instantiates the workers,
|
|
8
|
+
# creates an accumulator reporter and delegates tests to workers
|
|
9
|
+
# until the queue is empty.
|
|
10
|
+
class RactorExecutor < Executor
|
|
11
|
+
# @param options [Hash<Symbol, BasicObject>]
|
|
12
|
+
# @return [Loupe::Executor]
|
|
13
|
+
def initialize(options)
|
|
14
|
+
super
|
|
15
|
+
@workers = (0...[Etc.nprocessors, @queue.length].min).map do
|
|
16
|
+
Ractor.new(options) do |opts|
|
|
17
|
+
loop do
|
|
18
|
+
klass, method_name = Ractor.receive
|
|
19
|
+
Ractor.yield klass.run(method_name, opts)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Run the main process for executing tests
|
|
26
|
+
#
|
|
27
|
+
# Send the first tests to all workers from the queue and
|
|
28
|
+
# then keep selecting the idle Ractor until the queue is empty.
|
|
29
|
+
# Acumulate the reporters as tests are finalized.
|
|
30
|
+
# The last set of results are obtained outside the loop using `take`,
|
|
31
|
+
# since once the queue is empty `select` will no longer accumulate the result.
|
|
32
|
+
#
|
|
33
|
+
# @return [Integer]
|
|
34
|
+
def run
|
|
35
|
+
@workers.each do |r|
|
|
36
|
+
item = @queue.pop
|
|
37
|
+
r.send(item) unless item.nil?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
until @queue.empty?
|
|
41
|
+
idle_worker, tmp_reporter = Ractor.select(*@workers)
|
|
42
|
+
@reporter << tmp_reporter
|
|
43
|
+
idle_worker.send(@queue.pop)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
@workers.each { |w| @reporter << w.take }
|
|
47
|
+
|
|
48
|
+
@reporter.print_summary
|
|
49
|
+
@reporter.exit_status
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "loupe"
|
|
4
|
+
require "rake"
|
|
5
|
+
require "rake/tasklib"
|
|
6
|
+
|
|
7
|
+
module Loupe
|
|
8
|
+
# Loupe's test rake task
|
|
9
|
+
#
|
|
10
|
+
# Define a rake task so that we can hook into `rake test`
|
|
11
|
+
# an run the suite using Loupe. To hook it up, add this to the Rakefile
|
|
12
|
+
#
|
|
13
|
+
# require "loupe/rake_task"
|
|
14
|
+
#
|
|
15
|
+
# Loupe::RakeTask.new do |options|
|
|
16
|
+
# options << "--plain"
|
|
17
|
+
# options << "--ractor"
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# Then run with `bundle exec rake test`
|
|
21
|
+
#
|
|
22
|
+
class RakeTask < Rake::TaskLib
|
|
23
|
+
attr_accessor :name, :description, :libs
|
|
24
|
+
|
|
25
|
+
# @return [Loupe::RakeTask]
|
|
26
|
+
def initialize
|
|
27
|
+
super
|
|
28
|
+
|
|
29
|
+
@name = "test"
|
|
30
|
+
@description = "Run tests using Loupe"
|
|
31
|
+
@libs = %w[lib test]
|
|
32
|
+
@options = []
|
|
33
|
+
ARGV.shift if ARGV.first == "test"
|
|
34
|
+
yield(@options)
|
|
35
|
+
define
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# @return [Loupe::RakeTask]
|
|
41
|
+
def define
|
|
42
|
+
desc @description
|
|
43
|
+
task(@name) { Loupe::Cli.new(@options) }
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Loupe's reporter structure is heavily inspired by or adapted from Minitest. The
|
|
4
|
+
# originals license can be found below.
|
|
5
|
+
#
|
|
6
|
+
# Minitest https://github.com/seattlerb/minitest
|
|
7
|
+
#
|
|
8
|
+
# (The MIT License)
|
|
9
|
+
#
|
|
10
|
+
# Copyright © Ryan Davis, seattle.rb
|
|
11
|
+
#
|
|
12
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
13
|
+
# documentation files (the 'Software'), to deal in the Software without restriction, including without limitation
|
|
14
|
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
|
|
15
|
+
# to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
16
|
+
#
|
|
17
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of
|
|
18
|
+
# the Software.
|
|
19
|
+
#
|
|
20
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
|
|
21
|
+
# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
23
|
+
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
24
|
+
# IN THE SOFTWARE.
|
|
25
|
+
|
|
26
|
+
module Loupe
|
|
27
|
+
# Reporter
|
|
28
|
+
#
|
|
29
|
+
# Class that handles reporting test results
|
|
30
|
+
# and progress.
|
|
31
|
+
class Reporter
|
|
32
|
+
# @return [Integer]
|
|
33
|
+
attr_reader :test_count
|
|
34
|
+
|
|
35
|
+
# @return [Integer]
|
|
36
|
+
attr_reader :expectation_count
|
|
37
|
+
|
|
38
|
+
# @return [Integer]
|
|
39
|
+
attr_reader :success_count
|
|
40
|
+
|
|
41
|
+
# @return [Integer]
|
|
42
|
+
attr_reader :failure_count
|
|
43
|
+
|
|
44
|
+
# @return [Array<Loupe::Failure>]
|
|
45
|
+
attr_reader :failures
|
|
46
|
+
|
|
47
|
+
# @param options [Hash<Symbol, BasicObject>]
|
|
48
|
+
# @return [Loupe::Reporter]
|
|
49
|
+
def initialize(options = {})
|
|
50
|
+
@options = options
|
|
51
|
+
@color = Color.new(options[:color])
|
|
52
|
+
@options = options
|
|
53
|
+
@test_count = 0
|
|
54
|
+
@expectation_count = 0
|
|
55
|
+
@success_count = 0
|
|
56
|
+
@failure_count = 0
|
|
57
|
+
@failures = []
|
|
58
|
+
@start_time = Time.now
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @return [void]
|
|
62
|
+
def increment_test_count
|
|
63
|
+
@test_count += 1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @return [void]
|
|
67
|
+
def increment_expectation_count
|
|
68
|
+
@expectation_count += 1
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @return [void]
|
|
72
|
+
def increment_success_count
|
|
73
|
+
print(@color.p(".", :green))
|
|
74
|
+
@success_count += 1
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @param test [Loupe::Test]
|
|
78
|
+
# @return [void]
|
|
79
|
+
def increment_failure_count(test, message)
|
|
80
|
+
print(@color.p("F", :red))
|
|
81
|
+
@failures << Failure.new(test, message)
|
|
82
|
+
@failure_count += 1
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @param other [Loupe::Reporter]
|
|
86
|
+
# @return [Loupe::Reporter]
|
|
87
|
+
def <<(other)
|
|
88
|
+
@test_count += other.test_count
|
|
89
|
+
@expectation_count += other.expectation_count
|
|
90
|
+
@success_count += other.success_count
|
|
91
|
+
@failure_count += other.failure_count
|
|
92
|
+
@failures.concat(other.failures)
|
|
93
|
+
self
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @return [Integer]
|
|
97
|
+
def exit_status
|
|
98
|
+
@failure_count.zero? ? 0 : 1
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @return [void]
|
|
102
|
+
def print_summary
|
|
103
|
+
raise NotImplementedError, "Print must be implemented in the inheriting reporter class"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|