File: include.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 (213 lines) | stat: -rw-r--r-- 6,590 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
require 'rspec/matchers/built_in/count_expectation'

module RSpec
  module Matchers
    module BuiltIn
      # @api private
      # Provides the implementation for `include`.
      # Not intended to be instantiated directly.
      class Include < BaseMatcher # rubocop:disable Metrics/ClassLength
        include CountExpectation
        # @private
        attr_reader :expecteds

        # @api private
        def initialize(*expecteds)
          @expecteds = expecteds
        end

        # @api private
        # @return [Boolean]
        def matches?(actual)
          check_actual?(actual) &&
            if check_expected_count?
              expected_count_matches?(count_inclusions)
            else
              perform_match { |v| v }
            end
        end

        # @api private
        # @return [Boolean]
        def does_not_match?(actual)
          check_actual?(actual) &&
            if check_expected_count?
              !expected_count_matches?(count_inclusions)
            else
              perform_match { |v| !v }
            end
        end

        # @api private
        # @return [String]
        def description
          improve_hash_formatting("include#{readable_list_of(expecteds)}#{count_expectation_description}")
        end

        # @api private
        # @return [String]
        def failure_message
          format_failure_message("to") { super }
        end

        # @api private
        # @return [String]
        def failure_message_when_negated
          format_failure_message("not to") { super }
        end

        # @api private
        # @return [Boolean]
        def diffable?
          !diff_would_wrongly_highlight_matched_item?
        end

        # @api private
        # @return [Array, Hash]
        def expected
          if expecteds.one? && Hash === expecteds.first
            expecteds.first
          else
            expecteds
          end
        end

      private

        def check_actual?(actual)
          actual = actual.to_hash if convert_to_hash?(actual)
          @actual = actual
          @actual.respond_to?(:include?)
        end

        def check_expected_count?
          case
          when !has_expected_count?
            return false
          when expecteds.size != 1
            raise NotImplementedError, 'Count constraint supported only when testing for a single value being included'
          when actual.is_a?(Hash)
            raise NotImplementedError, 'Count constraint on hash keys not implemented'
          end
          true
        end

        def format_failure_message(preposition)
          msg = if actual.respond_to?(:include?)
                  "expected #{description_of @actual} #{preposition}" \
                  " include#{readable_list_of @divergent_items}" \
                  "#{count_failure_reason('it is included') if has_expected_count?}"
                else
                  "#{yield}, but it does not respond to `include?`"
                end
          improve_hash_formatting(msg)
        end

        def readable_list_of(items)
          described_items = surface_descriptions_in(items)
          if described_items.all? { |item| item.is_a?(Hash) }
            " #{described_items.inject(:merge).inspect}"
          else
            EnglishPhrasing.list(described_items)
          end
        end

        def perform_match(&block)
          @divergent_items = excluded_from_actual(&block)
          @divergent_items.empty?
        end

        def excluded_from_actual
          return [] unless @actual.respond_to?(:include?)

          expecteds.inject([]) do |memo, expected_item|
            if comparing_hash_to_a_subset?(expected_item)
              expected_item.each do |(key, value)|
                memo << { key => value } unless yield actual_hash_includes?(key, value)
              end
            elsif comparing_hash_keys?(expected_item)
              memo << expected_item unless yield actual_hash_has_key?(expected_item)
            else
              memo << expected_item unless yield actual_collection_includes?(expected_item)
            end
            memo
          end
        end

        def comparing_hash_to_a_subset?(expected_item)
          actual.is_a?(Hash) && expected_item.is_a?(Hash)
        end

        def actual_hash_includes?(expected_key, expected_value)
          actual_value =
            actual.fetch(expected_key) do
              actual.find(Proc.new { return false }) { |actual_key, _| values_match?(expected_key, actual_key) }[1]
            end
          values_match?(expected_value, actual_value)
        end

        def comparing_hash_keys?(expected_item)
          actual.is_a?(Hash) && !expected_item.is_a?(Hash)
        end

        def actual_hash_has_key?(expected_key)
          # We check `key?` first for perf:
          # `key?` is O(1), but `any?` is O(N).

          has_exact_key =
            begin
              actual.key?(expected_key)
            rescue
              false
            end

          has_exact_key || actual.keys.any? { |key| values_match?(expected_key, key) }
        end

        def actual_collection_includes?(expected_item)
          return true if actual.include?(expected_item)

          # String lacks an `any?` method...
          return false unless actual.respond_to?(:any?)

          actual.any? { |value| values_match?(expected_item, value) }
        end

        if RUBY_VERSION < '1.9'
          def count_enumerable(expected_item)
            actual.select { |value| values_match?(expected_item, value) }.size
          end
        else
          def count_enumerable(expected_item)
            actual.count { |value| values_match?(expected_item, value) }
          end
        end

        def count_inclusions
          @divergent_items = expected
          case actual
          when String
            actual.scan(expected.first).length
          when Enumerable
            count_enumerable(Hash === expected ? expected : expected.first)
          else
            raise NotImplementedError, 'Count constraints are implemented for Enumerable and String values only'
          end
        end

        def diff_would_wrongly_highlight_matched_item?
          return false unless actual.is_a?(String) && expected.is_a?(Array)

          lines = actual.split("\n")
          expected.any? do |str|
            actual.include?(str) && lines.none? { |line| line == str }
          end
        end

        def convert_to_hash?(obj)
          !obj.respond_to?(:include?) && obj.respond_to?(:to_hash)
        end
      end
    end
  end
end