rspec-capturing-formatter 1.0.0
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 +21 -0
- data/README.md +111 -0
- data/Rakefile +80 -0
- data/lib/rspec/capturing_formatter/configuration.rb +77 -0
- data/lib/rspec/capturing_formatter/renderer.rb +457 -0
- data/lib/rspec/capturing_formatter/sanitizer.rb +378 -0
- data/lib/rspec/capturing_formatter/stream_proxy.rb +414 -0
- data/lib/rspec/capturing_formatter/version.rb +9 -0
- data/lib/rspec/capturing_formatter/windows_command_line.rb +65 -0
- data/lib/rspec/capturing_formatter/windows_terminal.rb +87 -0
- data/lib/rspec/capturing_formatter.rb +319 -0
- metadata +120 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rspec/core"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
|
|
6
|
+
require_relative "capturing_formatter/version"
|
|
7
|
+
require_relative "capturing_formatter/configuration"
|
|
8
|
+
require_relative "capturing_formatter/sanitizer"
|
|
9
|
+
require_relative "capturing_formatter/stream_proxy"
|
|
10
|
+
require_relative "capturing_formatter/windows_terminal"
|
|
11
|
+
require_relative "capturing_formatter/renderer"
|
|
12
|
+
require_relative "capturing_formatter/windows_command_line"
|
|
13
|
+
|
|
14
|
+
module RSpec
|
|
15
|
+
# Interprets RSpec notifications and attributes captured writes; Renderer owns presentation.
|
|
16
|
+
class CapturingFormatter
|
|
17
|
+
class << self
|
|
18
|
+
def configuration
|
|
19
|
+
@configuration ||= CapturingFormatter::Configuration.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def configure
|
|
23
|
+
yield(configuration)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr_reader :output
|
|
28
|
+
|
|
29
|
+
def initialize(output, capture_manager: CapturingFormatter::CaptureManager.instance)
|
|
30
|
+
@output = output
|
|
31
|
+
@manager = capture_manager
|
|
32
|
+
manager_was_active = @manager.active?
|
|
33
|
+
manager_generation = @manager.generation
|
|
34
|
+
@renderer = CapturingFormatter::Renderer.new(
|
|
35
|
+
output,
|
|
36
|
+
self.class.configuration,
|
|
37
|
+
capture_manager: @manager
|
|
38
|
+
)
|
|
39
|
+
@groups = []
|
|
40
|
+
@seen_groups = {}
|
|
41
|
+
@current_example = nil
|
|
42
|
+
@started = false
|
|
43
|
+
@suite_heading_emitted = false
|
|
44
|
+
@report_visible = false
|
|
45
|
+
@failed_reruns = []
|
|
46
|
+
@seed_notification = nil
|
|
47
|
+
@lease = @manager.activate(output, self)
|
|
48
|
+
# Construction mutates process-global streams, so clean them up even for exceptions outside StandardError.
|
|
49
|
+
# standard:disable Lint/RescueException
|
|
50
|
+
rescue Exception
|
|
51
|
+
# standard:enable Lint/RescueException
|
|
52
|
+
if defined?(@lease) && @lease
|
|
53
|
+
@manager.deactivate(@lease)
|
|
54
|
+
elsif @manager && !manager_was_active
|
|
55
|
+
@manager.rollback_if_unchanged(manager_generation)
|
|
56
|
+
end
|
|
57
|
+
raise
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def start(notification)
|
|
61
|
+
with_capture_lock do
|
|
62
|
+
@started = true
|
|
63
|
+
@count = notification.count if notification.respond_to?(:count)
|
|
64
|
+
@load_time = notification.load_time if notification.respond_to?(:load_time)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def example_group_started(notification)
|
|
69
|
+
with_capture_lock { @groups << notification.group }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def example_group_finished(_notification)
|
|
73
|
+
with_capture_lock { @groups.pop }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def example_started(notification)
|
|
77
|
+
with_capture_lock do
|
|
78
|
+
example = notification.example
|
|
79
|
+
@groups.each { |group| @seen_groups[group.object_id] = true }
|
|
80
|
+
@current_example = example
|
|
81
|
+
@report_visible = true
|
|
82
|
+
@renderer.example_started(path_for(example))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def example_passed(notification)
|
|
87
|
+
with_capture_lock { finish_example(notification.example, :passed) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def example_failed(notification)
|
|
91
|
+
with_capture_lock do
|
|
92
|
+
example = notification.example
|
|
93
|
+
finish_example(example, :failed, notification)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def example_pending(notification)
|
|
98
|
+
with_capture_lock do
|
|
99
|
+
example = notification.example
|
|
100
|
+
reason = example.execution_result.pending_message.to_s
|
|
101
|
+
# RSpec represents a skipped example as pending without a pending exception.
|
|
102
|
+
skipped = example.execution_result.pending_exception.nil?
|
|
103
|
+
@renderer.pending(
|
|
104
|
+
reason,
|
|
105
|
+
rerun_command(example),
|
|
106
|
+
skipped: skipped,
|
|
107
|
+
run_time: example.execution_result.run_time
|
|
108
|
+
)
|
|
109
|
+
unless skipped || pending_failure_output == :skip
|
|
110
|
+
lines = failure_lines(notification, example)
|
|
111
|
+
if pending_failure_output == :no_backtrace
|
|
112
|
+
# RSpec's formatted backtrace lines start with `# ` after styling and indentation are removed.
|
|
113
|
+
lines = lines.reject { |line| strip_ansi(line.to_s).lstrip.start_with?("# ") }
|
|
114
|
+
end
|
|
115
|
+
@renderer.failure(lines)
|
|
116
|
+
end
|
|
117
|
+
@current_example = nil
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def message(notification)
|
|
122
|
+
with_capture_lock do
|
|
123
|
+
inside = !@current_example.nil?
|
|
124
|
+
@renderer.message(notification.message, inside_example: inside)
|
|
125
|
+
@report_visible = true
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def stop(_notification)
|
|
130
|
+
with_capture_lock { @manager.deactivate(@lease) }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def dump_profile(notification)
|
|
134
|
+
with_capture_lock { @renderer.profile(notification) }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def dump_summary(notification)
|
|
138
|
+
with_capture_lock do
|
|
139
|
+
total = notification.respond_to?(:example_count) ? notification.example_count : notification.examples.to_i
|
|
140
|
+
failed = notification.respond_to?(:failure_count) ? notification.failure_count : notification.failed_examples.to_i
|
|
141
|
+
pending = notification.respond_to?(:pending_count) ? notification.pending_count : notification.pending_examples.to_i
|
|
142
|
+
@renderer.summary(
|
|
143
|
+
total: total,
|
|
144
|
+
succeeded: total - failed - pending,
|
|
145
|
+
failed: failed,
|
|
146
|
+
pending: pending,
|
|
147
|
+
duration: notification.duration,
|
|
148
|
+
load_time: notification.load_time,
|
|
149
|
+
errors: notification.errors_outside_of_examples_count
|
|
150
|
+
)
|
|
151
|
+
@renderer.reruns(@failed_reruns.uniq)
|
|
152
|
+
if @seed_notification&.respond_to?(:seed_used?) && @seed_notification.seed_used?
|
|
153
|
+
@renderer.seed(@seed_notification.seed)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def seed(notification)
|
|
159
|
+
with_capture_lock { @seed_notification = notification }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def close(_notification = nil)
|
|
163
|
+
with_capture_lock { @manager.deactivate(@lease) }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def capture(source, value, encoding)
|
|
167
|
+
with_capture_lock do
|
|
168
|
+
if @current_example
|
|
169
|
+
@renderer.capture(source.to_s, value, encoding)
|
|
170
|
+
else
|
|
171
|
+
ensure_suite_or_context
|
|
172
|
+
label = (source == :stdout) ? "suite stdout" : "suite stderr"
|
|
173
|
+
@renderer.capture(label, value, encoding)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def finish_capture
|
|
179
|
+
with_capture_lock { @renderer.finish_capture }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def flush_pending
|
|
183
|
+
with_capture_lock { @renderer.flush_pending }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
private
|
|
187
|
+
|
|
188
|
+
def finish_example(example, status, notification = nil)
|
|
189
|
+
@renderer.result(status, example.execution_result.run_time)
|
|
190
|
+
if notification && status == :failed
|
|
191
|
+
@renderer.rerun_inline(rerun_command(example))
|
|
192
|
+
@renderer.failure(failure_lines(notification, example))
|
|
193
|
+
@failed_reruns << rerun_command(example)
|
|
194
|
+
elsif status == :failed
|
|
195
|
+
@failed_reruns << rerun_command(example)
|
|
196
|
+
end
|
|
197
|
+
@current_example = nil
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def with_capture_lock(&block)
|
|
201
|
+
# RSpec notifications and captured writes share this lock so report fragments cannot interleave.
|
|
202
|
+
@manager.synchronize(&block)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Normalize presenter output for inline rendering, including older presenters that reject a colorizer.
|
|
206
|
+
def failure_lines(notification, example)
|
|
207
|
+
lines = if notification.respond_to?(:fully_formatted_lines)
|
|
208
|
+
notification.fully_formatted_lines(nil, @renderer.failure_colorizer)
|
|
209
|
+
else
|
|
210
|
+
Array(notification.message_lines)
|
|
211
|
+
end
|
|
212
|
+
lines = Array(lines)
|
|
213
|
+
lines.shift while lines.first.to_s.empty?
|
|
214
|
+
description = example.full_description.to_s if example.respond_to?(:full_description)
|
|
215
|
+
if description && !description.empty? && strip_ansi(lines.first.to_s).strip == description
|
|
216
|
+
lines.shift
|
|
217
|
+
end
|
|
218
|
+
lines
|
|
219
|
+
rescue ArgumentError
|
|
220
|
+
value = Array(notification.fully_formatted_lines(nil))
|
|
221
|
+
value.shift while value.first.to_s.empty?
|
|
222
|
+
value
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def ensure_suite_or_context
|
|
226
|
+
# Suite output gets a heading only when it starts the report; unseen active groups get context headings.
|
|
227
|
+
if @groups.empty?
|
|
228
|
+
heading = !@suite_heading_emitted && !@report_visible
|
|
229
|
+
@renderer.suite_started(heading: heading)
|
|
230
|
+
@suite_heading_emitted = true if heading
|
|
231
|
+
else
|
|
232
|
+
heading = @groups.any? { |group| !@seen_groups[group.object_id] }
|
|
233
|
+
@renderer.context_started(context_path, heading: heading)
|
|
234
|
+
@groups.each { |group| @seen_groups[group.object_id] = true }
|
|
235
|
+
end
|
|
236
|
+
@report_visible = true
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def context_path
|
|
240
|
+
components = @groups.map { |group| group.description.to_s }.reject(&:empty?)
|
|
241
|
+
components.join(path_separator)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def path_for(example)
|
|
245
|
+
components = @groups.map { |group| group.description.to_s }.reject(&:empty?)
|
|
246
|
+
description = example.description.to_s
|
|
247
|
+
description = rerun_argument(example) if description.empty?
|
|
248
|
+
(components + [description]).reject(&:empty?).join(path_separator)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def rerun_command(example)
|
|
252
|
+
argument = rerun_argument(example)
|
|
253
|
+
return CapturingFormatter::WindowsCommandLine.rerun_command(argument) if Gem.win_platform?
|
|
254
|
+
|
|
255
|
+
"rspec #{Shellwords.escape(argument)}"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def rerun_argument(example)
|
|
259
|
+
# A duplicated location cannot identify one example, so prefer its stable RSpec ID.
|
|
260
|
+
location = if example.respond_to?(:location_rerun_argument)
|
|
261
|
+
example.location_rerun_argument
|
|
262
|
+
elsif example.respond_to?(:location)
|
|
263
|
+
example.location
|
|
264
|
+
else
|
|
265
|
+
example.id
|
|
266
|
+
end
|
|
267
|
+
duplicate = if defined?(RSpec.world) && RSpec.world.respond_to?(:all_examples)
|
|
268
|
+
RSpec.world.all_examples.count { |item| item.location_rerun_argument == location } > 1
|
|
269
|
+
end
|
|
270
|
+
(duplicate && example.respond_to?(:id)) ? example.id : location
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def path_separator
|
|
274
|
+
# Prefer a readable ASCII separator when the destination cannot encode the configured value.
|
|
275
|
+
separator = self.class.configuration.separator
|
|
276
|
+
encoding = @renderer.output.external_encoding if @renderer.output.respond_to?(:external_encoding)
|
|
277
|
+
return separator unless encoding
|
|
278
|
+
|
|
279
|
+
separator.encode(encoding)
|
|
280
|
+
separator
|
|
281
|
+
rescue EncodingError, TypeError
|
|
282
|
+
" > "
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def pending_failure_output
|
|
286
|
+
# An explicit formatter setting wins; otherwise inherit RSpec's setting when available.
|
|
287
|
+
configuration = self.class.configuration
|
|
288
|
+
return configuration.pending_failure_output if configuration.pending_failure_output_configured?
|
|
289
|
+
return configuration.pending_failure_output unless defined?(RSpec) &&
|
|
290
|
+
RSpec.respond_to?(:configuration) && RSpec.configuration.respond_to?(:pending_failure_output)
|
|
291
|
+
|
|
292
|
+
RSpec.configuration.pending_failure_output
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def strip_ansi(value)
|
|
296
|
+
value.gsub(/\e\[[0-?]*+[ -\/]*+[@-~]/, "")
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Requiring the formatter installs inactive pass-through proxies before RSpec constructs it.
|
|
301
|
+
CapturingFormatter::CaptureManager.install!
|
|
302
|
+
# Failure and expected-pending details are rendered inline, so their dump events are intentionally omitted.
|
|
303
|
+
RSpec::Core::Formatters.register(
|
|
304
|
+
CapturingFormatter,
|
|
305
|
+
:start,
|
|
306
|
+
:example_group_started,
|
|
307
|
+
:example_group_finished,
|
|
308
|
+
:example_started,
|
|
309
|
+
:example_passed,
|
|
310
|
+
:example_failed,
|
|
311
|
+
:example_pending,
|
|
312
|
+
:message,
|
|
313
|
+
:stop,
|
|
314
|
+
:dump_profile,
|
|
315
|
+
:dump_summary,
|
|
316
|
+
:seed,
|
|
317
|
+
:close
|
|
318
|
+
)
|
|
319
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rspec-capturing-formatter
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Hongli Lai
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rspec-core
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.12'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.12'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rspec
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.12'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.12'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: fiddle
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: logger
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
description: A CI-friendly RSpec formatter that keeps test progress readable when
|
|
83
|
+
examples write logs.
|
|
84
|
+
executables: []
|
|
85
|
+
extensions: []
|
|
86
|
+
extra_rdoc_files: []
|
|
87
|
+
files:
|
|
88
|
+
- LICENSE.txt
|
|
89
|
+
- README.md
|
|
90
|
+
- Rakefile
|
|
91
|
+
- lib/rspec/capturing_formatter.rb
|
|
92
|
+
- lib/rspec/capturing_formatter/configuration.rb
|
|
93
|
+
- lib/rspec/capturing_formatter/renderer.rb
|
|
94
|
+
- lib/rspec/capturing_formatter/sanitizer.rb
|
|
95
|
+
- lib/rspec/capturing_formatter/stream_proxy.rb
|
|
96
|
+
- lib/rspec/capturing_formatter/version.rb
|
|
97
|
+
- lib/rspec/capturing_formatter/windows_command_line.rb
|
|
98
|
+
- lib/rspec/capturing_formatter/windows_terminal.rb
|
|
99
|
+
homepage: https://github.com/rspec-capturing-formatter/rspec-capturing-formatter
|
|
100
|
+
licenses:
|
|
101
|
+
- MIT
|
|
102
|
+
metadata: {}
|
|
103
|
+
rdoc_options: []
|
|
104
|
+
require_paths:
|
|
105
|
+
- lib
|
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '0'
|
|
116
|
+
requirements: []
|
|
117
|
+
rubygems_version: 4.0.16
|
|
118
|
+
specification_version: 4
|
|
119
|
+
summary: A CI-friendly RSpec formatter that shows logs
|
|
120
|
+
test_files: []
|