module_test.rb 13.8 KB
Newer Older
1
# frozen_string_literal: true
2

3
require_relative "../abstract_unit"
4
require "active_support/core_ext/module"
5

6
Somewhere = Struct.new(:street, :city) do
7
  attr_accessor :name
8
end
9

10
Someone = Struct.new(:name, :place) do
11 12 13 14 15
  delegate :street, :city, :to_f, to: :place
  delegate :name=, to: :place, prefix: true
  delegate :upcase, to: "place.city"
  delegate :table_name, to: :class
  delegate :table_name, to: :class, prefix: true
16 17

  def self.table_name
18
    "some_table"
19
  end
20

21
  self::FAILED_DELEGATE_LINE = __LINE__ + 1
22
  delegate :foo, to: :place
23

24
  self::FAILED_DELEGATE_LINE_2 = __LINE__ + 1
25
  delegate :bar, to: :place, allow_nil: true
26

27 28 29 30
  def kw_send(method:)
    public_send(method)
  end

31
  private
32 33 34
    def private_name
      "Private"
    end
35 36
end

37
Invoice = Struct.new(:client) do
38 39
  delegate :street, :city, :name, to: :client, prefix: true
  delegate :street, :city, :name, to: :client, prefix: :customer
40 41
end

42
Project = Struct.new(:description, :person) do
43 44
  delegate :name, to: :person, allow_nil: true
  delegate :to_f, to: :description, allow_nil: true
45 46
end

47
Developer = Struct.new(:client) do
48
  delegate :name, to: :client, prefix: nil
49 50
end

51
Event = Struct.new(:case) do
52
  delegate :foo, to: :case
53 54
end

55
Tester = Struct.new(:client) do
56
  delegate :name, to: :client, prefix: false
57 58

  def foo; 1; end
59 60
end

61
Product = Struct.new(:name) do
62 63
  delegate :name, to: :manufacturer, prefix: true
  delegate :name, to: :type, prefix: true
64 65 66 67 68 69 70 71 72

  def manufacturer
    @manufacturer ||= begin
      nil.unknown_method
    end
  end

  def type
    @type ||= begin
73
      nil.type_name
74 75 76 77
    end
  end
end

78 79 80 81 82 83 84 85 86 87 88 89 90 91
module ExtraMissing
  def method_missing(sym, *args)
    if sym == :extra_missing
      42
    else
      super
    end
  end

  def respond_to_missing?(sym, priv = false)
    sym == :extra_missing || super
  end
end

92
DecoratedTester = Struct.new(:client) do
93 94
  include ExtraMissing

95 96 97
  delegate_missing_to :client
end

98 99 100 101 102 103 104 105 106 107
class DecoratedMissingAllowNil
  delegate_missing_to :case, allow_nil: true

  attr_reader :case

  def initialize(kase)
    @case = kase
  end
end

108 109 110 111 112 113 114 115 116 117
class DecoratedReserved
  delegate_missing_to :case

  attr_reader :case

  def initialize(kase)
    @case = kase
  end
end

118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
class Maze
  attr_accessor :cavern, :passages
end

class Cavern
  delegate_missing_to :target

  attr_reader :maze

  def initialize(maze)
    @maze = maze
  end

  def target
    @maze.passages = :twisty
  end
end

136 137 138 139 140 141 142 143 144 145
class Block
  def hello?
    true
  end
end

HasBlock = Struct.new(:block) do
  delegate :hello?, to: :block
end

146
class ParameterSet
147
  delegate :[], :[]=, to: :@params
148 149

  def initialize
150
    @params = { foo: "bar" }
151 152 153
  end
end

154
class Name
155
  delegate :upcase, to: :@full_name
156 157 158 159 160 161

  def initialize(first, last)
    @full_name = "#{first} #{last}"
  end
end

162 163 164
class SideEffect
  attr_reader :ints

165 166
  delegate :to_i, to: :shift, allow_nil: true
  delegate :to_s, to: :shift
167 168 169 170 171 172 173 174 175 176

  def initialize
    @ints = [1, 2, 3]
  end

  def shift
    @ints.shift
  end
end

177
class ModuleTest < ActiveSupport::TestCase
178 179 180 181
  def setup
    @david = Someone.new("David", Somewhere.new("Paulina", "Chicago"))
  end

182
  def test_delegation_to_methods
183 184
    assert_equal "Paulina", @david.street
    assert_equal "Chicago", @david.city
185
  end
186

187 188 189 190 191
  def test_delegation_to_assignment_method
    @david.place_name = "Fred"
    assert_equal "Fred", @david.place.name
  end

192 193 194 195 196 197 198 199 200 201 202
  def test_delegation_to_index_get_method
    @params = ParameterSet.new
    assert_equal "bar", @params[:foo]
  end

  def test_delegation_to_index_set_method
    @params = ParameterSet.new
    @params[:foo] = "baz"
    assert_equal "baz", @params[:foo]
  end

203
  def test_delegation_down_hierarchy
204
    assert_equal "CHICAGO", @david.upcase
205
  end
206

207 208 209 210
  def test_delegation_to_instance_variable
    david = Name.new("David", "Hansson")
    assert_equal "DAVID HANSSON", david.upcase
  end
211

212
  def test_delegation_to_class_method
213 214
    assert_equal "some_table", @david.table_name
    assert_equal "some_table", @david.class_table_name
215 216
  end

217
  def test_missing_delegation_target
218 219 220 221
    assert_raise(ArgumentError) do
      Name.send :delegate, :nowhere
    end
    assert_raise(ArgumentError) do
222
      Name.send :delegate, :noplace, tos: :hollywood
223
    end
224
  end
225

226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
  def test_delegation_target_when_prefix_is_true
    assert_nothing_raised do
      Name.send :delegate, :go, to: :you, prefix: true
    end
    assert_nothing_raised do
      Name.send :delegate, :go, to: :_you, prefix: true
    end
    assert_raise(ArgumentError) do
      Name.send :delegate, :go, to: :You, prefix: true
    end
    assert_raise(ArgumentError) do
      Name.send :delegate, :go, to: :@you, prefix: true
    end
  end

241
  def test_delegation_prefix
242
    invoice = Invoice.new(@david)
243 244 245
    assert_equal "David", invoice.client_name
    assert_equal "Paulina", invoice.client_street
    assert_equal "Chicago", invoice.client_city
246 247
  end

248
  def test_delegation_custom_prefix
249
    invoice = Invoice.new(@david)
250 251 252
    assert_equal "David", invoice.customer_name
    assert_equal "Paulina", invoice.customer_street
    assert_equal "Chicago", invoice.customer_city
253 254
  end

255
  def test_delegation_prefix_with_nil_or_false
256 257
    assert_equal "David", Developer.new(@david).name
    assert_equal "David", Tester.new(@david).name
258 259
  end

260 261 262 263 264 265
  def test_delegation_prefix_with_instance_variable
    assert_raise ArgumentError do
      Class.new do
        def initialize(client)
          @client = client
        end
266
        delegate :name, :address, to: :@client, prefix: true
267 268 269 270
      end
    end
  end

271 272
  def test_delegation_with_allow_nil
    rails = Project.new("Rails", Someone.new("David"))
273
    assert_equal "David", rails.name
274 275 276 277 278 279 280
  end

  def test_delegation_with_allow_nil_and_nil_value
    rails = Project.new("Rails")
    assert_nil rails.name
  end

G
Godfrey Chan 已提交
281
  # Ensures with check for nil, not for a falsy target.
282 283 284 285 286
  def test_delegation_with_allow_nil_and_false_value
    project = Project.new(false, false)
    assert_raise(NoMethodError) { project.name }
  end

L
Li Ellis Gallardo 已提交
287 288 289 290 291
  def test_delegation_with_allow_nil_and_invalid_value
    rails = Project.new("Rails", "David")
    assert_raise(NoMethodError) { rails.name }
  end

292 293
  def test_delegation_with_allow_nil_and_nil_value_and_prefix
    Project.class_eval do
294
      delegate :name, to: :person, allow_nil: true, prefix: true
295 296 297 298 299 300 301
    end
    rails = Project.new("Rails")
    assert_nil rails.person_name
  end

  def test_delegation_without_allow_nil_and_nil_value
    david = Someone.new("David")
302
    assert_raise(Module::DelegationError) { david.street }
303 304
  end

305 306 307 308 309 310 311 312 313 314
  def test_delegation_to_method_that_exists_on_nil
    nil_person = Someone.new(nil)
    assert_equal 0.0, nil_person.to_f
  end

  def test_delegation_to_method_that_exists_on_nil_when_allowing_nil
    nil_project = Project.new(nil)
    assert_equal 0.0, nil_project.to_f
  end

315 316 317 318 319 320
  def test_delegation_does_not_raise_error_when_removing_singleton_instance_methods
    parent = Class.new do
      def self.parent_method; end
    end

    assert_nothing_raised do
S
Santiago Pastorino 已提交
321
      Class.new(parent) do
322
        class << self
323
          delegate :parent_method, to: :superclass
324 325 326 327 328
        end
      end
    end
  end

329
  def test_delegation_line_number
R
Rafael Mendonça França 已提交
330
    _, line = Someone.instance_method(:foo).source_location
331 332 333 334
    assert_equal Someone::FAILED_DELEGATE_LINE, line
  end

  def test_delegate_line_with_nil
R
Rafael Mendonça França 已提交
335
    _, line = Someone.instance_method(:bar).source_location
336 337 338
    assert_equal Someone::FAILED_DELEGATE_LINE_2, line
  end

339 340 341 342 343
  def test_delegation_exception_backtrace
    someone = Someone.new("foo", "bar")
    someone.foo
  rescue NoMethodError => e
    file_and_line = "#{__FILE__}:#{Someone::FAILED_DELEGATE_LINE}"
344
    # We can't simply check the first line of the backtrace, because JRuby reports the call to __send__ in the backtrace.
345
    assert e.backtrace.any? { |a| a.include?(file_and_line) },
346
           "[#{e.backtrace.inspect}] did not include [#{file_and_line}]"
347 348
  end

349 350 351 352 353
  def test_delegation_exception_backtrace_with_allow_nil
    someone = Someone.new("foo", "bar")
    someone.bar
  rescue NoMethodError => e
    file_and_line = "#{__FILE__}:#{Someone::FAILED_DELEGATE_LINE_2}"
354
    # We can't simply check the first line of the backtrace, because JRuby reports the call to __send__ in the backtrace.
355
    assert e.backtrace.any? { |a| a.include?(file_and_line) },
356
           "[#{e.backtrace.inspect}] did not include [#{file_and_line}]"
357 358
  end

359 360 361 362 363 364
  def test_delegation_invokes_the_target_exactly_once
    se = SideEffect.new

    assert_equal 1, se.to_i
    assert_equal [2, 3], se.ints

365
    assert_equal "2", se.to_s
366 367 368
    assert_equal [3], se.ints
  end

369
  def test_delegation_doesnt_mask_nested_no_method_error_on_nil_receiver
370
    product = Product.new("Widget")
371 372 373 374 375 376 377 378

    # Nested NoMethodError is a different name from the delegation
    assert_raise(NoMethodError) { product.manufacturer_name }

    # Nested NoMethodError is the same name as the delegation
    assert_raise(NoMethodError) { product.type_name }
  end

379 380
  def test_delegation_with_method_arguments
    has_block = HasBlock.new(Block.new)
381
    assert_predicate has_block, :hello?
382 383
  end

384
  def test_delegate_missing_to_with_method
385 386 387
    assert_equal "David", DecoratedTester.new(@david).name
  end

388
  def test_delegate_missing_to_with_reserved_methods
389 390 391
    assert_equal "David", DecoratedReserved.new(@david).name
  end

392 393 394 395
  def test_delegate_missing_to_with_keyword_methods
    assert_equal "David", DecoratedReserved.new(@david).kw_send(method: "name")
  end

396
  def test_delegate_missing_to_does_not_delegate_to_private_methods
397 398 399 400 401 402 403
    e = assert_raises(NoMethodError) do
      DecoratedReserved.new(@david).private_name
    end

    assert_match(/undefined method `private_name' for/, e.message)
  end

404
  def test_delegate_missing_to_does_not_delegate_to_fake_methods
405 406 407 408 409 410 411
    e = assert_raises(NoMethodError) do
      DecoratedReserved.new(@david).my_fake_method
    end

    assert_match(/undefined method `my_fake_method' for/, e.message)
  end

412 413 414 415 416 417 418 419
  def test_delegate_missing_to_raises_delegation_error_if_target_nil
    e = assert_raises(Module::DelegationError) do
      DecoratedTester.new(nil).name
    end

    assert_equal "name delegated to client, but client is nil", e.message
  end

420 421 422 423
  def test_delegate_missing_to_returns_nil_if_allow_nil_and_nil_target
    assert_nil DecoratedMissingAllowNil.new(nil).name
  end

424
  def test_delegate_missing_to_affects_respond_to
D
Daniel Colson 已提交
425 426 427
    assert_respond_to DecoratedTester.new(@david), :name
    assert_not_respond_to DecoratedTester.new(@david), :private_name
    assert_not_respond_to DecoratedTester.new(@david), :my_fake_method
428 429 430 431 432 433

    assert DecoratedTester.new(@david).respond_to?(:name, true)
    assert_not DecoratedTester.new(@david).respond_to?(:private_name, true)
    assert_not DecoratedTester.new(@david).respond_to?(:my_fake_method, true)
  end

434
  def test_delegate_missing_to_respects_superclass_missing
435 436 437 438 439
    assert_equal 42, DecoratedTester.new(@david).extra_missing

    assert_respond_to DecoratedTester.new(@david), :extra_missing
  end

440 441 442 443 444 445 446 447 448 449 450
  def test_delegate_missing_to_does_not_interfere_with_marshallization
    maze = Maze.new
    maze.cavern = Cavern.new(maze)

    array = [maze, nil]
    serialized_array = Marshal.dump(array)
    deserialized_array = Marshal.load(serialized_array)

    assert_nil deserialized_array[1]
  end

451 452 453 454
  def test_delegate_with_case
    event = Event.new(Tester.new)
    assert_equal 1, event.foo
  end
455 456 457 458 459 460 461

  def test_private_delegate
    location = Class.new do
      def initialize(place)
        @place = place
      end

462
      private(*delegate(:street, :city, to: :@place))
463 464 465 466
    end

    place = location.new(Somewhere.new("Such street", "Sad city"))

D
Daniel Colson 已提交
467 468
    assert_not_respond_to place, :street
    assert_not_respond_to place, :city
469 470 471 472 473 474 475 476 477 478 479

    assert place.respond_to?(:street, true) # Asking for private method
    assert place.respond_to?(:city, true)
  end

  def test_private_delegate_prefixed
    location = Class.new do
      def initialize(place)
        @place = place
      end

480
      private(*delegate(:street, :city, to: :@place, prefix: :the))
481 482 483 484
    end

    place = location.new(Somewhere.new("Such street", "Sad city"))

D
Daniel Colson 已提交
485 486
    assert_not_respond_to place, :street
    assert_not_respond_to place, :city
487

D
Daniel Colson 已提交
488
    assert_not_respond_to place, :the_street
489
    assert place.respond_to?(:the_street, true)
D
Daniel Colson 已提交
490
    assert_not_respond_to place, :the_city
491 492
    assert place.respond_to?(:the_city, true)
  end
493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535

  def test_private_delegate_with_private_option
    location = Class.new do
      def initialize(place)
        @place = place
      end

      delegate(:street, :city, to: :@place, private: true)
    end

    place = location.new(Somewhere.new("Such street", "Sad city"))

    assert_not_respond_to place, :street
    assert_not_respond_to place, :city

    assert place.respond_to?(:street, true) # Asking for private method
    assert place.respond_to?(:city, true)
  end

  def test_some_public_some_private_delegate_with_private_option
    location = Class.new do
      def initialize(place)
        @place = place
      end

      delegate(:street, to: :@place)
      delegate(:city, to: :@place, private: true)
    end

    place = location.new(Somewhere.new("Such street", "Sad city"))

    assert_respond_to place, :street
    assert_not_respond_to place, :city

    assert place.respond_to?(:city, true) # Asking for private method
  end

  def test_private_delegate_prefixed_with_private_option
    location = Class.new do
      def initialize(place)
        @place = place
      end

536 537
      delegate(:street, :city, to: :@place, prefix: :the, private: true)
    end
538 539 540 541 542 543 544 545

    place = location.new(Somewhere.new("Such street", "Sad city"))

    assert_not_respond_to place, :the_street
    assert place.respond_to?(:the_street, true)
    assert_not_respond_to place, :the_city
    assert place.respond_to?(:the_city, true)
  end
546 547 548 549 550 551 552 553 554 555 556 557 558 559

  def test_delegate_with_private_option_returns_names_of_delegate_methods
    location = Class.new do
      def initialize(place)
        @place = place
      end
    end

    assert_equal [:street, :city],
      location.delegate(:street, :city, to: :@place, private: true)

    assert_equal [:the_street, :the_city],
      location.delegate(:street, :city, to: :@place, prefix: :the, private: true)
  end
560
end