rspec-expectations 3.5.0 → 3.9.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.
Files changed (37) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/Changelog.md +138 -2
  5. data/README.md +37 -20
  6. data/lib/rspec/expectations.rb +2 -1
  7. data/lib/rspec/expectations/block_snippet_extractor.rb +253 -0
  8. data/lib/rspec/expectations/configuration.rb +14 -0
  9. data/lib/rspec/expectations/expectation_target.rb +2 -2
  10. data/lib/rspec/expectations/fail_with.rb +9 -1
  11. data/lib/rspec/expectations/handler.rb +2 -2
  12. data/lib/rspec/expectations/minitest_integration.rb +1 -1
  13. data/lib/rspec/expectations/syntax.rb +2 -2
  14. data/lib/rspec/expectations/version.rb +1 -1
  15. data/lib/rspec/matchers.rb +97 -97
  16. data/lib/rspec/matchers/built_in/all.rb +1 -0
  17. data/lib/rspec/matchers/built_in/base_matcher.rb +14 -2
  18. data/lib/rspec/matchers/built_in/be.rb +2 -2
  19. data/lib/rspec/matchers/built_in/be_instance_of.rb +5 -1
  20. data/lib/rspec/matchers/built_in/be_kind_of.rb +5 -1
  21. data/lib/rspec/matchers/built_in/change.rb +127 -53
  22. data/lib/rspec/matchers/built_in/compound.rb +6 -2
  23. data/lib/rspec/matchers/built_in/contain_exactly.rb +18 -2
  24. data/lib/rspec/matchers/built_in/exist.rb +5 -1
  25. data/lib/rspec/matchers/built_in/has.rb +1 -1
  26. data/lib/rspec/matchers/built_in/include.rb +6 -0
  27. data/lib/rspec/matchers/built_in/raise_error.rb +1 -1
  28. data/lib/rspec/matchers/built_in/respond_to.rb +13 -4
  29. data/lib/rspec/matchers/built_in/satisfy.rb +27 -4
  30. data/lib/rspec/matchers/built_in/yield.rb +43 -30
  31. data/lib/rspec/matchers/composable.rb +6 -20
  32. data/lib/rspec/matchers/dsl.rb +72 -4
  33. data/lib/rspec/matchers/english_phrasing.rb +3 -3
  34. data/lib/rspec/matchers/expecteds_for_multiple_diffs.rb +16 -7
  35. data/lib/rspec/matchers/generated_descriptions.rb +1 -2
  36. metadata +22 -18
  37. metadata.gz.sig +0 -0
@@ -56,6 +56,20 @@ module RSpec
56
56
  end
57
57
  end
58
58
 
59
+ # Configures the maximum character length that RSpec will print while
60
+ # formatting an object. You can set length to nil to prevent RSpec from
61
+ # doing truncation.
62
+ # @param [Fixnum] length the number of characters to limit the formatted output to.
63
+ # @example
64
+ # RSpec.configure do |rspec|
65
+ # rspec.expect_with :rspec do |c|
66
+ # c.max_formatted_output_length = 200
67
+ # end
68
+ # end
69
+ def max_formatted_output_length=(length)
70
+ RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = length
71
+ end
72
+
59
73
  # The list of configured syntaxes.
60
74
  # @return [Array<Symbol>] the list of configured syntaxes.
61
75
  # @example
@@ -48,7 +48,7 @@ module RSpec
48
48
 
49
49
  # Defines instance {ExpectationTarget} instance methods. These are defined
50
50
  # in a module so we can include it in `Minitest::Expectation` when
51
- # `rspec/expectations/minitest_integration` is laoded in order to
51
+ # `rspec/expectations/minitest_integration` is loaded in order to
52
52
  # support usage with Minitest.
53
53
  module InstanceMethods
54
54
  # Runs the given expectation, passing if `matcher` returns true.
@@ -112,7 +112,7 @@ module RSpec
112
112
  def enforce_block_expectation(matcher)
113
113
  return if supports_block_expectations?(matcher)
114
114
 
115
- raise ExpectationNotMetError, "You must pass an argument rather than a block to use the provided " \
115
+ raise ExpectationNotMetError, "You must pass an argument rather than a block to `expect` to use the provided " \
116
116
  "matcher (#{RSpec::Support::ObjectFormatter.format(matcher)}), or the matcher must implement " \
117
117
  "`supports_block_expectations?`."
118
118
  end
@@ -1,10 +1,18 @@
1
1
  module RSpec
2
2
  module Expectations
3
3
  class << self
4
+ # @private
5
+ class Differ
6
+ # @private
7
+ OBJECT_PREPARER = lambda do |object|
8
+ RSpec::Matchers::Composable.surface_descriptions_in(object)
9
+ end
10
+ end
11
+
4
12
  # @private
5
13
  def differ
6
14
  RSpec::Support::Differ.new(
7
- :object_preparer => lambda { |object| RSpec::Matchers::Composable.surface_descriptions_in(object) },
15
+ :object_preparer => Differ::OBJECT_PREPARER,
8
16
  :color => RSpec::Matchers.configuration.color?
9
17
  )
10
18
  end
@@ -52,7 +52,7 @@ module RSpec
52
52
  end
53
53
 
54
54
  def self.verb
55
- "should"
55
+ 'is expected to'
56
56
  end
57
57
 
58
58
  def self.should_method
@@ -82,7 +82,7 @@ module RSpec
82
82
  end
83
83
 
84
84
  def self.verb
85
- "should not"
85
+ 'is expected not to'
86
86
  end
87
87
 
88
88
  def self.should_method
@@ -53,6 +53,6 @@ module RSpec
53
53
  module Expectations
54
54
  remove_const :ExpectationNotMetError
55
55
  # Exception raised when an expectation fails.
56
- ExpectationNotMetError = ::Minitest::Assertion
56
+ const_set :ExpectationNotMetError, ::Minitest::Assertion
57
57
  end
58
58
  end
@@ -106,7 +106,7 @@ if defined?(BasicObject)
106
106
  # that this syntax does not always play nice with delegate/proxy objects.
107
107
  # We recommend you use the non-monkeypatching `:expect` syntax instead.
108
108
  class BasicObject
109
- # @method should
109
+ # @method should(matcher, message)
110
110
  # Passes if `matcher` returns true. Available on every `Object`.
111
111
  # @example
112
112
  # actual.should eq expected
@@ -118,7 +118,7 @@ if defined?(BasicObject)
118
118
  # @note This is only available when you have enabled the `:should` syntax.
119
119
  # @see RSpec::Matchers
120
120
 
121
- # @method should_not
121
+ # @method should_not(matcher, message)
122
122
  # Passes if `matcher` returns false. Available on every `Object`.
123
123
  # @example
124
124
  # actual.should_not eq expected
@@ -2,7 +2,7 @@ module RSpec
2
2
  module Expectations
3
3
  # @private
4
4
  module Version
5
- STRING = '3.5.0'
5
+ STRING = '3.9.0'
6
6
  end
7
7
  end
8
8
  end
@@ -41,9 +41,9 @@ module RSpec
41
41
  #
42
42
  # expect("a string").to be_an_instance_of(String) # =>"a string".instance_of?(String) # passes
43
43
  #
44
- # expect(3).to be_a_kind_of(Fixnum) # => 3.kind_of?(Numeric) | passes
45
- # expect(3).to be_a_kind_of(Numeric) # => 3.kind_of?(Numeric) | passes
46
- # expect(3).to be_an_instance_of(Fixnum) # => 3.instance_of?(Fixnum) | passes
44
+ # expect(3).to be_a_kind_of(Integer) # => 3.kind_of?(Numeric) | passes
45
+ # expect(3).to be_a_kind_of(Numeric) # => 3.kind_of?(Numeric) | passes
46
+ # expect(3).to be_an_instance_of(Integer) # => 3.instance_of?(Integer) | passes
47
47
  # expect(3).not_to be_an_instance_of(Numeric) # => 3.instance_of?(Numeric) | fails
48
48
  #
49
49
  # RSpec will also create custom matchers for predicates like `has_key?`. To
@@ -62,6 +62,26 @@ module RSpec
62
62
  # RSpec::Matchers.alias_matcher :a_user_who_is_an_admin, :be_an_admin
63
63
  # expect(user_list).to include(a_user_who_is_an_admin)
64
64
  #
65
+ # ## Alias Matchers
66
+ #
67
+ # With {RSpec::Matchers.alias_matcher}, you can easily create an
68
+ # alternate name for a given matcher.
69
+ #
70
+ # The description will also change according to the new name:
71
+ #
72
+ # RSpec::Matchers.alias_matcher :a_list_that_sums_to, :sum_to
73
+ # sum_to(3).description # => "sum to 3"
74
+ # a_list_that_sums_to(3).description # => "a list that sums to 3"
75
+ #
76
+ # or you can specify a custom description like this:
77
+ #
78
+ # RSpec::Matchers.alias_matcher :a_list_sorted_by, :be_sorted_by do |description|
79
+ # description.sub("be sorted by", "a list sorted by")
80
+ # end
81
+ #
82
+ # be_sorted_by(:age).description # => "be sorted by age"
83
+ # a_list_sorted_by(:age).description # => "a list sorted by age"
84
+ #
65
85
  # ## Custom Matchers
66
86
  #
67
87
  # When you find that none of the stock matchers provide a natural feeling
@@ -202,80 +222,53 @@ module RSpec
202
222
  # expressions, and also uses the noun-phrase wording in the matcher's `description`,
203
223
  # for readable failure messages. You can alias your custom matchers in similar fashion
204
224
  # using {RSpec::Matchers.alias_matcher}.
225
+ #
226
+ # ## Negated Matchers
227
+ #
228
+ # Sometimes if you want to test for the opposite using a more descriptive name
229
+ # instead of using `not_to`, you can use {RSpec::Matchers.define_negated_matcher}:
230
+ #
231
+ # RSpec::Matchers.define_negated_matcher :exclude, :include
232
+ # include(1, 2).description # => "include 1 and 2"
233
+ # exclude(1, 2).description # => "exclude 1 and 2"
234
+ #
235
+ # While the most obvious negated form may be to add a `not_` prefix,
236
+ # the failure messages you get with that form can be confusing (e.g.
237
+ # "expected [actual] to not [verb], but did not"). We've found it works
238
+ # best to find a more positive name for the negated form, such as
239
+ # `avoid_changing` rather than `not_change`.
240
+ #
205
241
  module Matchers
206
- # @method expect
207
- # Supports `expect(actual).to matcher` syntax by wrapping `actual` in an
208
- # `ExpectationTarget`.
209
- # @example
210
- # expect(actual).to eq(expected)
211
- # expect(actual).not_to eq(expected)
212
- # @return [ExpectationTarget]
213
- # @see ExpectationTarget#to
214
- # @see ExpectationTarget#not_to
242
+ extend ::RSpec::Matchers::DSL
215
243
 
216
- # Defines a matcher alias. The returned matcher's `description` will be overriden
217
- # to reflect the phrasing of the new name, which will be used in failure messages
218
- # when passed as an argument to another matcher in a composed matcher expression.
219
- #
220
- # @param new_name [Symbol] the new name for the matcher
221
- # @param old_name [Symbol] the original name for the matcher
222
- # @param options [Hash] options for the aliased matcher
223
- # @option options [Class] :klass the ruby class to use as the decorator. (Not normally used).
224
- # @yield [String] optional block that, when given, is used to define the overriden
225
- # logic. The yielded arg is the original description or failure message. If no
226
- # block is provided, a default override is used based on the old and new names.
227
- #
228
- # @example
229
- # RSpec::Matchers.alias_matcher :a_list_that_sums_to, :sum_to
230
- # sum_to(3).description # => "sum to 3"
231
- # a_list_that_sums_to(3).description # => "a list that sums to 3"
232
- #
233
- # @example
234
- # RSpec::Matchers.alias_matcher :a_list_sorted_by, :be_sorted_by do |description|
235
- # description.sub("be sorted by", "a list sorted by")
236
- # end
237
- #
238
- # be_sorted_by(:age).description # => "be sorted by age"
239
- # a_list_sorted_by(:age).description # => "a list sorted by age"
240
- #
241
244
  # @!macro [attach] alias_matcher
242
245
  # @!parse
243
246
  # alias $1 $2
244
- def self.alias_matcher(new_name, old_name, options={}, &description_override)
245
- description_override ||= lambda do |old_desc|
246
- old_desc.gsub(EnglishPhrasing.split_words(old_name), EnglishPhrasing.split_words(new_name))
247
- end
248
- klass = options.fetch(:klass) { AliasedMatcher }
249
-
250
- define_method(new_name) do |*args, &block|
251
- matcher = __send__(old_name, *args, &block)
252
- klass.new(matcher, description_override)
253
- end
247
+ # @!visibility private
248
+ # We define this override here so we can attach a YARD macro to it.
249
+ # It ensures that our docs list all the matcher aliases.
250
+ def self.alias_matcher(*args, &block)
251
+ super(*args, &block)
254
252
  end
255
253
 
256
- # Defines a negated matcher. The returned matcher's `description` and `failure_message`
257
- # will be overriden to reflect the phrasing of the new name, and the match logic will
258
- # be based on the original matcher but negated.
259
- #
260
- # @param negated_name [Symbol] the name for the negated matcher
261
- # @param base_name [Symbol] the name of the original matcher that will be negated
262
- # @yield [String] optional block that, when given, is used to define the overriden
263
- # logic. The yielded arg is the original description or failure message. If no
264
- # block is provided, a default override is used based on the old and new names.
265
- #
254
+ # @!method self.alias_matcher(new_name, old_name, options={}, &description_override)
255
+ # Extended from {RSpec::Matchers::DSL#alias_matcher}.
256
+
257
+ # @!method self.define(name, &declarations)
258
+ # Extended from {RSpec::Matchers::DSL#define}.
259
+
260
+ # @!method self.define_negated_matcher(negated_name, base_name, &description_override)
261
+ # Extended from {RSpec::Matchers::DSL#define_negated_matcher}.
262
+
263
+ # @method expect
264
+ # Supports `expect(actual).to matcher` syntax by wrapping `actual` in an
265
+ # `ExpectationTarget`.
266
266
  # @example
267
- # RSpec::Matchers.define_negated_matcher :exclude, :include
268
- # include(1, 2).description # => "include 1 and 2"
269
- # exclude(1, 2).description # => "exclude 1 and 2"
270
- #
271
- # @note While the most obvious negated form may be to add a `not_` prefix,
272
- # the failure messages you get with that form can be confusing (e.g.
273
- # "expected [actual] to not [verb], but did not"). We've found it works
274
- # best to find a more positive name for the negated form, such as
275
- # `avoid_changing` rather than `not_change`.
276
- def self.define_negated_matcher(negated_name, base_name, &description_override)
277
- alias_matcher(negated_name, base_name, :klass => AliasedNegatedMatcher, &description_override)
278
- end
267
+ # expect(actual).to eq(expected)
268
+ # expect(actual).not_to eq(expected)
269
+ # @return [Expectations::ExpectationTarget]
270
+ # @see Expectations::ExpectationTarget#to
271
+ # @see Expectations::ExpectationTarget#not_to
279
272
 
280
273
  # Allows multiple expectations in the provided block to fail, and then
281
274
  # aggregates them into a single exception, rather than aborting on the
@@ -367,7 +360,7 @@ module RSpec
367
360
  # Passes if actual.instance_of?(expected)
368
361
  #
369
362
  # @example
370
- # expect(5).to be_an_instance_of(Fixnum)
363
+ # expect(5).to be_an_instance_of(Integer)
371
364
  # expect(5).not_to be_an_instance_of(Numeric)
372
365
  # expect(5).not_to be_an_instance_of(Float)
373
366
  def be_an_instance_of(expected)
@@ -379,7 +372,7 @@ module RSpec
379
372
  # Passes if actual.kind_of?(expected)
380
373
  #
381
374
  # @example
382
- # expect(5).to be_a_kind_of(Fixnum)
375
+ # expect(5).to be_a_kind_of(Integer)
383
376
  # expect(5).to be_a_kind_of(Numeric)
384
377
  # expect(5).not_to be_a_kind_of(Float)
385
378
  def be_a_kind_of(expected)
@@ -487,7 +480,10 @@ module RSpec
487
480
  # == Notes
488
481
  #
489
482
  # Evaluates `receiver.message` or `block` before and after it
490
- # evaluates the block passed to `expect`.
483
+ # evaluates the block passed to `expect`. If the value is the same
484
+ # object, its before/after `hash` value is used to see if it has changed.
485
+ # Therefore, your object needs to properly implement `hash` to work correctly
486
+ # with this matcher.
491
487
  #
492
488
  # `expect( ... ).not_to change` supports the form that specifies `from`
493
489
  # (which specifies what you expect the starting, unchanged value to be)
@@ -585,7 +581,7 @@ module RSpec
585
581
  # information about equality in Ruby.
586
582
  #
587
583
  # @example
588
- # expect(5).to equal(5) # Fixnums are equal
584
+ # expect(5).to equal(5) # Integers are equal
589
585
  # expect("5").not_to equal("5") # Strings that look the same are not the same object
590
586
  def equal(expected)
591
587
  BuiltIn::Equal.new(expected)
@@ -688,7 +684,7 @@ module RSpec
688
684
  # :a => {
689
685
  # :b => a_collection_containing_exactly(
690
686
  # a_string_starting_with("f"),
691
- # an_instance_of(Fixnum)
687
+ # an_instance_of(Integer)
692
688
  # ),
693
689
  # :c => { :d => (a_value < 3) }
694
690
  # }
@@ -809,7 +805,7 @@ module RSpec
809
805
  # @example
810
806
  # expect(5).to satisfy { |n| n > 3 }
811
807
  # expect(5).to satisfy("be greater than 3") { |n| n > 3 }
812
- def satisfy(description="satisfy block", &block)
808
+ def satisfy(description=nil, &block)
813
809
  BuiltIn::Satisfy.new(description, &block)
814
810
  end
815
811
  alias_matcher :an_object_satisfying, :satisfy
@@ -905,7 +901,7 @@ module RSpec
905
901
  # @example
906
902
  # expect { |b| 5.tap(&b) }.to yield_with_args # because #tap yields an arg
907
903
  # expect { |b| 5.tap(&b) }.to yield_with_args(5) # because 5 == 5
908
- # expect { |b| 5.tap(&b) }.to yield_with_args(Fixnum) # because Fixnum === 5
904
+ # expect { |b| 5.tap(&b) }.to yield_with_args(Integer) # because Integer === 5
909
905
  # expect { |b| File.open("f.txt", &b) }.to yield_with_args(/txt/) # because /txt/ === "f.txt"
910
906
  #
911
907
  # expect { |b| User.transaction(&b) }.not_to yield_with_args # because it yields no args
@@ -1007,31 +1003,35 @@ module RSpec
1007
1003
  is_a_matcher?(obj) && obj.respond_to?(:description)
1008
1004
  end
1009
1005
 
1010
- if RSpec::Support::Ruby.mri? && RUBY_VERSION[0, 3] == '1.9'
1011
- # @api private
1012
- # Note that `included` doesn't work for this because it is triggered
1013
- # _after_ `RSpec::Matchers` is an ancestor of the inclusion host, rather
1014
- # than _before_, like `append_features`. It's important we check this before
1015
- # in order to find the cases where it was already previously included.
1016
- def self.append_features(mod)
1017
- return super if mod < self # `mod < self` indicates a re-inclusion.
1006
+ class << self
1007
+ private
1018
1008
 
1019
- subclasses = ObjectSpace.each_object(Class).select { |c| c < mod && c < self }
1020
- return super unless subclasses.any?
1009
+ if RSpec::Support::Ruby.mri? && RUBY_VERSION[0, 3] == '1.9'
1010
+ # Note that `included` doesn't work for this because it is triggered
1011
+ # _after_ `RSpec::Matchers` is an ancestor of the inclusion host, rather
1012
+ # than _before_, like `append_features`. It's important we check this before
1013
+ # in order to find the cases where it was already previously included.
1014
+ # @api private
1015
+ def append_features(mod)
1016
+ return super if mod < self # `mod < self` indicates a re-inclusion.
1021
1017
 
1022
- subclasses.reject! { |s| subclasses.any? { |s2| s < s2 } } # Filter to the root ancestor.
1023
- subclasses = subclasses.map { |s| "`#{s}`" }.join(", ")
1018
+ subclasses = ObjectSpace.each_object(Class).select { |c| c < mod && c < self }
1019
+ return super unless subclasses.any?
1024
1020
 
1025
- RSpec.warning "`#{self}` has been included in a superclass (`#{mod}`) " \
1026
- "after previously being included in subclasses (#{subclasses}), " \
1027
- "which can trigger infinite recursion from `super` due to an MRI 1.9 bug " \
1028
- "(https://redmine.ruby-lang.org/issues/3351). To work around this, " \
1029
- "either upgrade to MRI 2.0+, include a dup of the module (e.g. " \
1030
- "`include #{self}.dup`), or find a way to include `#{self}` in `#{mod}` " \
1031
- "before it is included in subclasses (#{subclasses}). See " \
1032
- "https://github.com/rspec/rspec-expectations/issues/814 for more info"
1021
+ subclasses.reject! { |s| subclasses.any? { |s2| s < s2 } } # Filter to the root ancestor.
1022
+ subclasses = subclasses.map { |s| "`#{s}`" }.join(", ")
1033
1023
 
1034
- super
1024
+ RSpec.warning "`#{self}` has been included in a superclass (`#{mod}`) " \
1025
+ "after previously being included in subclasses (#{subclasses}), " \
1026
+ "which can trigger infinite recursion from `super` due to an MRI 1.9 bug " \
1027
+ "(https://redmine.ruby-lang.org/issues/3351). To work around this, " \
1028
+ "either upgrade to MRI 2.0+, include a dup of the module (e.g. " \
1029
+ "`include #{self}.dup`), or find a way to include `#{self}` in `#{mod}` " \
1030
+ "before it is included in subclasses (#{subclasses}). See " \
1031
+ "https://github.com/rspec/rspec-expectations/issues/814 for more info"
1032
+
1033
+ super
1034
+ end
1035
1035
  end
1036
1036
  end
1037
1037
  end
@@ -73,6 +73,7 @@ module RSpec
73
73
 
74
74
  def initialize_copy(other)
75
75
  @matcher = @matcher.clone
76
+ @failed_objects = @failed_objects.clone
76
77
  super
77
78
  end
78
79
 
@@ -22,6 +22,9 @@ module RSpec
22
22
  # @private
23
23
  attr_reader :actual, :expected, :rescued_exception
24
24
 
25
+ # @private
26
+ attr_writer :matcher_name
27
+
25
28
  def initialize(expected=UNDEFINED)
26
29
  @expected = expected unless UNDEFINED.equal?(expected)
27
30
  end
@@ -95,6 +98,15 @@ module RSpec
95
98
  @matcher_name ||= underscore(name.split('::').last)
96
99
  end
97
100
 
101
+ # @private
102
+ def matcher_name
103
+ if defined?(@matcher_name)
104
+ @matcher_name
105
+ else
106
+ self.class.matcher_name
107
+ end
108
+ end
109
+
98
110
  # @private
99
111
  # Borrowed from ActiveSupport.
100
112
  def self.underscore(camel_cased_word)
@@ -153,7 +165,7 @@ module RSpec
153
165
  # you often only need to override `description`.
154
166
  # @return [String]
155
167
  def failure_message
156
- "expected #{description_of @actual} to #{description}"
168
+ "expected #{description_of @actual} to #{description}".dup
157
169
  end
158
170
 
159
171
  # @api private
@@ -162,7 +174,7 @@ module RSpec
162
174
  # you often only need to override `description`.
163
175
  # @return [String]
164
176
  def failure_message_when_negated
165
- "expected #{description_of @actual} not to #{description}"
177
+ "expected #{description_of @actual} not to #{description}".dup
166
178
  end
167
179
 
168
180
  # @private
@@ -145,7 +145,7 @@ module RSpec
145
145
  def matches?(actual)
146
146
  @actual = actual
147
147
  @actual.__send__ @operator, @expected
148
- rescue ArgumentError
148
+ rescue ArgumentError, NoMethodError
149
149
  false
150
150
  end
151
151
 
@@ -270,7 +270,7 @@ module RSpec
270
270
  def validity_message
271
271
  return nil if predicate_accessible?
272
272
 
273
- msg = "expected #{actual_formatted} to respond to `#{predicate}`"
273
+ msg = "expected #{actual_formatted} to respond to `#{predicate}`".dup
274
274
 
275
275
  if private_predicate?
276
276
  msg << " but `#{predicate}` is a private method"