File: metadata.rb

package info (click to toggle)
ruby-rspec-core 2.14.7-2
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 1,756 kB
  • ctags: 1,195
  • sloc: ruby: 12,708; makefile: 14
file content (299 lines) | stat: -rw-r--r-- 9,595 bytes parent folder | download
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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
module RSpec
  module Core
    # Each ExampleGroup class and Example instance owns an instance of
    # Metadata, which is Hash extended to support lazy evaluation of values
    # associated with keys that may or may not be used by any example or group.
    #
    # In addition to metadata that is used internally, this also stores
    # user-supplied metadata, e.g.
    #
    #     describe Something, :type => :ui do
    #       it "does something", :slow => true do
    #         # ...
    #       end
    #     end
    #
    # `:type => :ui` is stored in the Metadata owned by the example group, and
    # `:slow => true` is stored in the Metadata owned by the example. These can
    # then be used to select which examples are run using the `--tag` option on
    # the command line, or several methods on `Configuration` used to filter a
    # run (e.g. `filter_run_including`, `filter_run_excluding`, etc).
    #
    # @see Example#metadata
    # @see ExampleGroup.metadata
    # @see FilterManager
    # @see Configuration#filter_run_including
    # @see Configuration#filter_run_excluding
    class Metadata < Hash

      def self.relative_path(line)
        line = line.sub(File.expand_path("."), ".")
        line = line.sub(/\A([^:]+:\d+)$/, '\\1')
        return nil if line == '-e:1'
        line
      rescue SecurityError
        nil
      end

      # @private
      module MetadataHash

        # @private
        # Supports lazy evaluation of some values. Extended by
        # ExampleMetadataHash and GroupMetadataHash, which get mixed in to
        # Metadata for ExampleGroups and Examples (respectively).
        def [](key)
          store_computed(key) unless has_key?(key)
          super
        end

        def fetch(key, *args)
          store_computed(key) unless has_key?(key)
          super
        end

        private

        def store_computed(key)
          case key
          when :location
            store(:location, location)
          when :file_path, :line_number
            file_path, line_number = file_and_line_number
            store(:file_path, file_path)
            store(:line_number, line_number)
          when :execution_result
            store(:execution_result, {})
          when :describes, :described_class
            klass = described_class
            store(:described_class, klass)
            # TODO (2011-11-07 DC) deprecate :describes as a key
            store(:describes, klass)
          when :full_description
            store(:full_description, full_description)
          when :description
            store(:description, build_description_from(*self[:description_args]))
          when :description_args
            store(:description_args, [])
          end
        end

        def location
          "#{self[:file_path]}:#{self[:line_number]}"
        end

        def file_and_line_number
          first_caller_from_outside_rspec =~ /(.+?):(\d+)(|:\d+)/
          return [Metadata::relative_path($1), $2.to_i]
        end

        def first_caller_from_outside_rspec
          self[:caller].detect {|l| l !~ /\/(lib|vendor_ruby)\/rspec\/core/}
        end

        def method_description_after_module?(parent_part, child_part)
          return false unless parent_part.is_a?(Module)
          child_part =~ /^(#|::|\.)/
        end

        def build_description_from(first_part = '', *parts)
          description, _ = parts.inject([first_part.to_s, first_part]) do |(desc, last_part), this_part|
            this_part = this_part.to_s
            this_part = (' ' + this_part) unless method_description_after_module?(last_part, this_part)
            [(desc + this_part), this_part]
          end

          description
        end
      end

      # Mixed in to Metadata for an Example (extends MetadataHash) to support
      # lazy evaluation of some values.
      module ExampleMetadataHash
        include MetadataHash

        def described_class
          self[:example_group].described_class
        end

        def full_description
          build_description_from(self[:example_group][:full_description], *self[:description_args])
        end
      end

      # Mixed in to Metadata for an ExampleGroup (extends MetadataHash) to
      # support lazy evaluation of some values.
      module GroupMetadataHash
        include MetadataHash

        def described_class
          container_stack.each do |g|
            [:described_class, :describes].each do |key|
              if g.has_key?(key)
                value = g[key]
                return value unless value.nil?
              end
            end
          end

          container_stack.reverse.each do |g|
            candidate = g[:description_args].first
            return candidate unless String === candidate || Symbol === candidate
          end

          nil
        end

        def full_description
          build_description_from(*container_stack.reverse.map {|a| a[:description_args]}.flatten)
        end

        def container_stack
          @container_stack ||= begin
                                 groups = [group = self]
                                 while group.has_key?(:example_group)
                                   groups << group[:example_group]
                                   group = group[:example_group]
                                 end
                                 groups
                               end
        end
      end

      def initialize(parent_group_metadata=nil)
        if parent_group_metadata
          update(parent_group_metadata)
          store(:example_group, {:example_group => parent_group_metadata[:example_group].extend(GroupMetadataHash)}.extend(GroupMetadataHash))
        else
          store(:example_group, {}.extend(GroupMetadataHash))
        end

        yield self if block_given?
      end

      # @private
      def process(*args)
        user_metadata = args.last.is_a?(Hash) ? args.pop : {}
        ensure_valid_keys(user_metadata)

        self[:example_group].store(:description_args, args)
        self[:example_group].store(:caller, user_metadata.delete(:caller) || caller)

        update(user_metadata)
      end

      # @private
      def for_example(description, user_metadata)
        dup.extend(ExampleMetadataHash).configure_for_example(description, user_metadata)
      end

      # @private
      def any_apply?(filters)
        filters.any? {|k,v| filter_applies?(k,v)}
      end

      # @private
      def all_apply?(filters)
        filters.all? {|k,v| filter_applies?(k,v)}
      end

      # @private
      def filter_applies?(key, value, metadata=self)
        return metadata.filter_applies_to_any_value?(key, value) if Array === metadata[key] && !(Proc === value)
        return metadata.line_number_filter_applies?(value)       if key == :line_numbers
        return metadata.location_filter_applies?(value)          if key == :locations
        return metadata.filters_apply?(key, value)               if Hash === value

        return false unless metadata.has_key?(key)

        case value
        when Regexp
          metadata[key] =~ value
        when Proc
          case value.arity
          when 0 then value.call
          when 2 then value.call(metadata[key], metadata)
          else value.call(metadata[key])
          end
        else
          metadata[key].to_s == value.to_s
        end
      end

      # @private
      def filters_apply?(key, value)
        value.all? {|k, v| filter_applies?(k, v, self[key])}
      end

      # @private
      def filter_applies_to_any_value?(key, value)
        self[key].any? {|v| filter_applies?(key, v, {key => value})}
      end

      # @private
      def location_filter_applies?(locations)
        # it ignores location filters for other files
        line_number = example_group_declaration_line(locations)
        line_number ? line_number_filter_applies?(line_number) : true
      end

      # @private
      def line_number_filter_applies?(line_numbers)
        preceding_declaration_lines = line_numbers.map {|n| RSpec.world.preceding_declaration_line(n)}
        !(relevant_line_numbers & preceding_declaration_lines).empty?
      end

      protected

      def configure_for_example(description, user_metadata)
        store(:description_args, [description]) if description
        store(:caller, user_metadata.delete(:caller) || caller)
        update(user_metadata)
      end

      private

      RESERVED_KEYS = [
        :description,
        :example_group,
        :execution_result,
        :file_path,
        :full_description,
        :line_number,
        :location
      ]

      def ensure_valid_keys(user_metadata)
        RESERVED_KEYS.each do |key|
          if user_metadata.has_key?(key)
            raise <<-EOM
            #{"*"*50}
:#{key} is not allowed

RSpec reserves some hash keys for its own internal use,
including :#{key}, which is used on:

            #{caller(0)[4]}.

Here are all of RSpec's reserved hash keys:

            #{RESERVED_KEYS.join("\n  ")}
            #{"*"*50}
            EOM
          end
        end
      end

      def example_group_declaration_line(locations)
        locations[File.expand_path(self[:example_group][:file_path])] if self[:example_group]
      end

      # TODO - make this a method on metadata - the problem is
      # metadata[:example_group] is not always a kind of GroupMetadataHash.
      def relevant_line_numbers(metadata=self)
        [metadata[:line_number]] + (metadata[:example_group] ? relevant_line_numbers(metadata[:example_group]) : [])
      end

    end
  end
end