File: validators.rb

package info (click to toggle)
ruby-flexmock 3.0.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 836 kB
  • sloc: ruby: 7,572; makefile: 6
file content (269 lines) | stat: -rw-r--r-- 8,128 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
#!/usr/bin/env ruby

#---
# Copyright 2003-2013 by Jim Weirich (jim.weirich@gmail.com).
# All rights reserved.

# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#+++

require 'set'
require 'flexmock/noop'
require 'flexmock/spy_describers'

class FlexMock

  ####################################################################
  # Base class for all the count validators.
  #
  class CountValidator
    include FlexMock::SpyDescribers

    def initialize(expectation, limit)
      @exp = expectation
      @limit = limit
    end

    # If the expectation has been called +n+ times, is it still
    # eligible to be called again?  The default answer compares n to
    # the established limit.
    def eligible?(n)
      n < @limit
    end

    # Human readable description of the validator
    def describe
      case @limit
      when 0
        ".never"
      when 1
        ".once"
      when 2
        ".twice"
      else
        ".times(#{@limit})"
      end
    end

    def describe_limit
      @limit.to_s
    end

    class ValidationFailed < RuntimeError
    end

    def validate_count(n, &block)
      unless yield
        raise ValidationFailed, construct_validation_count_error_message(n)
      end
    end

    private

    # Build the error message for an invalid count
    def construct_validation_count_error_message(n)
      "Method '#{@exp}' called incorrect number of times\n" +
        "#{describe_limit} matching #{calls(@limit)} expected\n" +
        "#{n} matching #{calls(n)} found\n" +
        describe_calls(@exp.mock)
    end

    # Pluralize "call"
    def calls(n)
      n == 1 ? "call" : "calls"
    end
  end

  ####################################################################
  # Validator for exact call counts.
  #
  class ExactCountValidator < CountValidator
    # Validate that the method expectation was called exactly +n+
    # times.
    def validate(n)
      validate_count(n) { @limit == n }
    end
  end

  ####################################################################
  # Validator for call counts greater than or equal to a limit.
  #
  class AtLeastCountValidator < CountValidator
    # Validate the method expectation was called no more than +n+
    # times.
    def validate(n)
      validate_count(n) { n >= @limit }
    end

    # Human readable description of the validator.
    def describe
      if @limit == 0
        ".zero_or_more_times"
      else
        ".at_least#{super}"
      end
    end

    # If the expectation has been called +n+ times, is it still
    # eligible to be called again?  Since this validator only
    # establishes a lower limit, not an upper limit, then the answer
    # is always true.
    def eligible?(n)
      true
    end

    def describe_limit
      "At least #{@limit}"
    end
  end

  ####################################################################
  # Validator for call counts less than or equal to a limit.
  #
  class AtMostCountValidator < CountValidator
    # Validate the method expectation was called at least +n+ times.
    def validate(n)
      validate_count(n) { n <= @limit }
    end

    # Human readable description of the validator
    def describe
      ".at_most#{super}"
    end

    def describe_limit
      "At most #{@limit}"
    end
  end

  # Validate that the call matches a given signature
  #
  # The validator created by {#initialize} matches any method call
  class SignatureValidator
    class ValidationFailed < RuntimeError
    end

    # The number of required arguments
    attr_reader :required_arguments
    # The number of optional arguments
    attr_reader :optional_arguments
    # Whether there is a positional argument splat
    def splat?
      @splat
    end
    # The names of required keyword arguments
    # @return [Set<Symbol>]
    attr_reader :required_keyword_arguments
    # The names of optional keyword arguments
    # @return [Set<Symbol>]
    attr_reader :optional_keyword_arguments
    # Whether there is a splat for keyword arguments (double-star)
    def keyword_splat?
      @keyword_splat
    end

    # Whether this method may have keyword arguments
    def expects_keyword_arguments?
      keyword_splat? || !required_keyword_arguments.empty? || !optional_keyword_arguments.empty?
    end

    # Whether this method may have keyword arguments
    def requires_keyword_arguments?
      !required_keyword_arguments.empty?
    end

    def initialize(
        expectation,
        required_arguments: 0,
        optional_arguments: 0,
        splat: true,
        required_keyword_arguments: [],
        optional_keyword_arguments: [],
        keyword_splat: true)
      @exp = expectation
      @required_arguments = required_arguments
      @optional_arguments = optional_arguments
      @required_keyword_arguments = required_keyword_arguments.to_set
      @optional_keyword_arguments = optional_keyword_arguments.to_set
      @splat = splat
      @keyword_splat = keyword_splat
    end

    # Whether this tests anything
    #
    # It will return if this validator would validate any set of arguments
    def null?
      splat? && keyword_splat?
    end

    def describe
      ".with_signature(
          required_arguments: #{self.required_arguments},
          optional_arguments: #{self.optional_arguments},
          required_keyword_arguments: #{self.required_keyword_arguments.to_a},
          optional_keyword_arguments: #{self.optional_keyword_arguments.to_a},
          splat: #{self.splat?},
          keyword_splat: #{self.keyword_splat?})"
    end

    # Validates whether the given arguments match the expected signature
    #
    # @param [Array] args
    # @raise ValidationFailed
    def validate(args, kw, block)
      kw ||= Hash.new

      if expects_keyword_arguments? && requires_keyword_arguments? && kw.empty?
        raise ValidationFailed, "#{@exp} expects keyword arguments but none were provided"
      end

      if required_arguments > args.size
        raise ValidationFailed, "#{@exp} expects at least #{required_arguments} positional arguments but got only #{args.size}"
      end

      if !splat? && (required_arguments + optional_arguments) < args.size
        raise ValidationFailed, "#{@exp} expects at most #{required_arguments + optional_arguments} positional arguments but got #{args.size}"
      end

      missing_keyword_arguments = required_keyword_arguments.
        find_all { |k| !kw.has_key?(k) }
      if !missing_keyword_arguments.empty?
        raise ValidationFailed, "#{@exp} missing required keyword arguments #{missing_keyword_arguments.map(&:to_s).sort.join(", ")}"
      end
      if !keyword_splat?
        kw.each_key do |k|
          if !optional_keyword_arguments.include?(k) && !required_keyword_arguments.include?(k)
            raise ValidationFailed, "#{@exp} given unexpected keyword argument #{k}"
          end
        end
      end
    end

    # Create a validator that represents the signature of an existing method
    def self.from_instance_method(exp, instance_method)
      required_arguments, optional_arguments, splat = 0, 0, false
      required_keyword_arguments, optional_keyword_arguments, keyword_splat = Set.new, Set.new, false
      instance_method.parameters.each do |type, name|
        case type
        when :req then required_arguments += 1
        when :opt then optional_arguments += 1
        when :rest then splat = true
        when :keyreq then required_keyword_arguments << name
        when :key then optional_keyword_arguments << name
        when :keyrest then keyword_splat = true
        when :block
        else raise ArgumentError, "cannot interpret parameter type #{type}"
        end
      end
      new(exp,
          required_arguments: required_arguments,
          optional_arguments: optional_arguments,
          splat: splat,
          required_keyword_arguments: required_keyword_arguments,
          optional_keyword_arguments: optional_keyword_arguments,
          keyword_splat: keyword_splat)
    end
  end
end