File: formatters.rb

package info (click to toggle)
ruby-rspec 3.13.0c0e0m0s1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 6,856 kB
  • sloc: ruby: 70,868; sh: 1,423; makefile: 99
file content (279 lines) | stat: -rw-r--r-- 9,843 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
RSpec::Support.require_rspec_support "directory_maker"

# ## Built-in Formatters
#
# * progress (default) - Prints dots for passing examples, `F` for failures, `*`
#                        for pending.
# * documentation - Prints the docstrings passed to `describe` and `it` methods
#                   (and their aliases).
# * html
# * json - Useful for archiving data for subsequent analysis.
#
# The progress formatter is the default, but you can choose any one or more of
# the other formatters by passing with the `--format` (or `-f` for short)
# command-line option, e.g.
#
#     rspec --format documentation
#
# You can also send the output of multiple formatters to different streams, e.g.
#
#     rspec --format documentation --format html --out results.html
#
# This example sends the output of the documentation formatter to `$stdout`, and
# the output of the html formatter to results.html.
#
# ## Custom Formatters
#
# You can tell RSpec to use a custom formatter by passing its path and name to
# the `rspec` command. For example, if you define MyCustomFormatter in
# path/to/my_custom_formatter.rb, you would type this command:
#
#     rspec --require path/to/my_custom_formatter.rb --format MyCustomFormatter
#
# The reporter calls every formatter with this protocol:
#
# * To start
#   * `start(StartNotification)`
# * Once per example group
#   * `example_group_started(GroupNotification)`
# * Once per example
#   * `example_started(ExampleNotification)`
# * One of these per example, depending on outcome
#   * `example_passed(ExampleNotification)`
#   * `example_failed(FailedExampleNotification)`
#   * `example_pending(ExampleNotification)`
# * Optionally at any time
#   * `message(MessageNotification)`
# * At the end of the suite
#   * `stop(ExamplesNotification)`
#   * `start_dump(NullNotification)`
#   * `dump_pending(ExamplesNotification)`
#   * `dump_failures(ExamplesNotification)`
#   * `dump_summary(SummaryNotification)`
#   * `seed(SeedNotification)`
#   * `close(NullNotification)`
#
# Only the notifications to which you subscribe your formatter will be called
# on your formatter. To subscribe your formatter use:
# `RSpec::Core::Formatters#register` e.g.
#
# `RSpec::Core::Formatters.register FormatterClassName, :example_passed, :example_failed`
#
# We recommend you implement the methods yourself; for simplicity we provide the
# default formatter output via our notification objects but if you prefer you
# can subclass `RSpec::Core::Formatters::BaseTextFormatter` and override the
# methods you wish to enhance.
#
# @see RSpec::Core::Formatters::BaseTextFormatter
# @see RSpec::Core::Reporter
module RSpec::Core::Formatters
  autoload :DocumentationFormatter,   'rspec/core/formatters/documentation_formatter'
  autoload :HtmlFormatter,            'rspec/core/formatters/html_formatter'
  autoload :FallbackMessageFormatter, 'rspec/core/formatters/fallback_message_formatter'
  autoload :ProgressFormatter,        'rspec/core/formatters/progress_formatter'
  autoload :ProfileFormatter,         'rspec/core/formatters/profile_formatter'
  autoload :JsonFormatter,            'rspec/core/formatters/json_formatter'
  autoload :BisectDRbFormatter,       'rspec/core/formatters/bisect_drb_formatter'
  autoload :ExceptionPresenter,       'rspec/core/formatters/exception_presenter'
  autoload :FailureListFormatter,     'rspec/core/formatters/failure_list_formatter'

  # Register the formatter class
  # @param formatter_class [Class] formatter class to register
  # @param notifications [Array<Symbol>] one or more notifications to be
  #   registered to the specified formatter
  #
  # @see RSpec::Core::Formatters::BaseFormatter
  def self.register(formatter_class, *notifications)
    Loader.formatters[formatter_class] = notifications
  end

  # @api private
  #
  # `RSpec::Core::Formatters::Loader` is an internal class for
  # managing formatters used by a particular configuration. It is
  # not expected to be used directly, but only through the configuration
  # interface.
  class Loader
    # @api private
    #
    # Internal formatters are stored here when loaded.
    def self.formatters
      @formatters ||= {}
    end

    # @api private
    def initialize(reporter)
      @formatters = []
      @reporter = reporter
      self.default_formatter = 'progress'
    end

    # @return [Array] the loaded formatters
    attr_reader :formatters

    # @return [Reporter] the reporter
    attr_reader :reporter

    # @return [String] the default formatter to setup, defaults to `progress`
    attr_accessor :default_formatter

    # @private
    def prepare_default(output_stream, deprecation_stream)
      reporter.prepare_default(self, output_stream, deprecation_stream)
    end

    # @private
    def setup_default(output_stream, deprecation_stream)
      add default_formatter, output_stream if @formatters.empty?

      unless @formatters.any? { |formatter| DeprecationFormatter === formatter }
        add DeprecationFormatter, deprecation_stream, output_stream
      end

      unless existing_formatter_implements?(:message)
        add FallbackMessageFormatter, output_stream
      end

      return unless RSpec.configuration.profile_examples?
      return if existing_formatter_implements?(:dump_profile)

      add RSpec::Core::Formatters::ProfileFormatter, output_stream
    end

    # @private
    def add(formatter_to_use, *paths)
      # If a formatter instance was passed, we can register it directly,
      # with no need for any of the further processing that happens below.
      if Loader.formatters.key?(formatter_to_use.class)
        register formatter_to_use, notifications_for(formatter_to_use.class)
        return
      end

      formatter_class = find_formatter(formatter_to_use)

      args = paths.map { |p| p.respond_to?(:puts) ? p : open_stream(p) }

      if !Loader.formatters[formatter_class].nil?
        formatter = formatter_class.new(*args)
        register formatter, notifications_for(formatter_class)
      elsif defined?(RSpec::LegacyFormatters)
        formatter = RSpec::LegacyFormatters.load_formatter formatter_class, *args
        register formatter, formatter.notifications
      else
        call_site = "Formatter added at: #{::RSpec::CallerFilter.first_non_rspec_line}"

        RSpec.warn_deprecation <<-WARNING.gsub(/\s*\|/, ' ')
          |The #{formatter_class} formatter uses the deprecated formatter
          |interface not supported directly by RSpec 3.
          |
          |To continue to use this formatter you must install the
          |`rspec-legacy_formatters` gem, which provides support
          |for legacy formatters or upgrade the formatter to a
          |compatible version.
          |
          |#{call_site}
        WARNING
      end
    end

  private

    def find_formatter(formatter_to_use)
      built_in_formatter(formatter_to_use) ||
      custom_formatter(formatter_to_use)   ||
      (raise ArgumentError, "Formatter '#{formatter_to_use}' unknown - " \
                            "maybe you meant 'documentation' or 'progress'?.")
    end

    def register(formatter, notifications)
      return if duplicate_formatter_exists?(formatter)
      @reporter.register_listener formatter, *notifications
      @formatters << formatter
      formatter
    end

    def duplicate_formatter_exists?(new_formatter)
      @formatters.any? do |formatter|
        formatter.class == new_formatter.class &&
          has_matching_output?(formatter, new_formatter)
      end
    end

    def has_matching_output?(formatter, new_formatter)
      return true unless formatter.respond_to?(:output) && new_formatter.respond_to?(:output)
      formatter.output == new_formatter.output
    end

    def existing_formatter_implements?(notification)
      @reporter.registered_listeners(notification).any?
    end

    def built_in_formatter(key)
      case key.to_s
      when 'd', 'doc', 'documentation'
        DocumentationFormatter
      when 'h', 'html'
        HtmlFormatter
      when 'p', 'progress'
        ProgressFormatter
      when 'j', 'json'
        JsonFormatter
      when 'bisect-drb'
        BisectDRbFormatter
      when 'f', 'failures'
        FailureListFormatter
      end
    end

    def notifications_for(formatter_class)
      formatter_class.ancestors.inject(::RSpec::Core::Set.new) do |notifications, klass|
        notifications.merge Loader.formatters.fetch(klass) { ::RSpec::Core::Set.new }
      end
    end

    def custom_formatter(formatter_ref)
      if Class === formatter_ref
        formatter_ref
      elsif string_const?(formatter_ref)
        begin
          formatter_ref.gsub(/^::/, '').split('::').inject(Object) { |a, e| a.const_get e }
        rescue NameError
          require(path_for(formatter_ref)) ? retry : raise
        end
      end
    end

    def string_const?(str)
      str.is_a?(String) && /\A[A-Z][a-zA-Z0-9_:]*\z/ =~ str
    end

    def path_for(const_ref)
      underscore_with_fix_for_non_standard_rspec_naming(const_ref)
    end

    def underscore_with_fix_for_non_standard_rspec_naming(string)
      underscore(string).sub(%r{(^|/)r_spec($|/)}, '\\1rspec\\2')
    end

    # activesupport/lib/active_support/inflector/methods.rb, line 48
    def underscore(camel_cased_word)
      word = camel_cased_word.to_s.dup
      word.gsub!(/::/, '/')
      word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
      word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
      word.tr!("-", "_")
      word.downcase!
      word
    end

    def open_stream(path_or_wrapper)
      if RSpec::Core::OutputWrapper === path_or_wrapper
        path_or_wrapper.output = open_stream(path_or_wrapper.output)
        path_or_wrapper
      else
        RSpec::Support::DirectoryMaker.mkdir_p(File.dirname(path_or_wrapper))
        File.new(path_or_wrapper, 'w')
      end
    end
  end
end