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.
@@ -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: []