rubyjs / therubyracer

Embed the V8 Javascript Interpreter into Ruby
1.67k stars 190 forks source link

"Method" objects assigned to a Context appear in every subsequent Context #412

Closed abscondment closed 7 months ago

abscondment commented 8 years ago

Problem Setup

Say I have defined a method in Ruby, and I want to make it available in JS.

I might do that like this:

class UserProxy
  attr_reader :name
  def initialize(name)
    @name = name
  end
end

user = UserProxy.new('Brendan')
context = V8::Context.new
context['getMyName'] = user.method(:name)
context.scope.getMyName
# => "Brendan"

This seems very straightforward.

Say I try to do this multiple times, with different UserProxys. Maybe I create a new V8::Context each time, or I might simply assign a new method to context['getMyName']. Either way, it won't work as expected.

Expectation

I should be able to assign a different method to getMyName in this V8::Context, or in a new instance of V8::Context, and receive a different return value.

Reality

I cannot change getMyName -- it always returns "Brendan". Additionally, the original method is called after trying to assign getMyName to any brand new V8::Context.

I notice that if I wrap my ruby method in a lambda, things work. If I assign the Method object, however, it is broken.

Test Case

Here's a test case that demonstrates this in more detail:

gem 'therubyracer', '~> 0.12.2'#, platforms: :ruby
require 'therubyracer'
require 'set'
require 'minitest/autorun'
require 'ostruct'

Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)

class UserProxy
  attr_reader :name
  def initialize(name)
    @name = name
  end
end

class FakeContext
  attr_reader :scope
  def initialize
    @scope = OpenStruct.new
  end

  def []=(k,v)
    @scope[k] = v
  end
end

class BugTest < Minitest::Test
  def test_call_via_method_FAILS
    billy_context = by_method('Billy')
    bobby_context = by_method('Bobby')

    refute_equal billy_context.scope.name(),
                 bobby_context.scope.name(),
                 'Billy and Bobby really should have different names'
  end

  def test_call_many_methods_FAILS
    names = %w{ Brooke Bella Ben Bethany Blair Betsy }

    results = names.map do |name|
      by_method(name).scope.name()
    end

    assert_equal Set.new(names),
                 Set.new(results),
                 "Every name should be returned."
  end

  def test_call_via_lambda_SUCCEEDS
    billy_context = by_lambda('Billy')
    bobby_context = by_lambda('Bobby')

    refute_equal billy_context.scope.name(),
                 bobby_context.scope.name(),
                 'Billy and Bobby really should have different names'
  end

  def test_call_fake_via_method_SUCCEEDS
    billy_context = fake_by_method('Billy')
    bobby_context = fake_by_method('Bobby')

    refute_equal billy_context.scope.name(),
                 bobby_context.scope.name(),
                 'Billy and Bobby really should have different names'
  end

  protected

  def fake_by_method(name)
    user = UserProxy.new(name)
    context = FakeContext.new
    context['name'] = user.method(:name)
    context
  end

  def by_method(name)
    user = UserProxy.new(name)
    context = V8::Context.new
    context['name'] = user.method(:name)
    context
  end

  def by_lambda(name)
    user = UserProxy.new(name)
    context = V8::Context.new
    context['name'] = lambda { user.name }
    context
  end
end

You'll notice that, depending on which test executes first, a different name method might get stuck.

E.g. if I run with --seed 54026, "Brooke" is the only name ever returned. If I run with --seed 29795, "Billy" gets set first and therefore is stuck forever.

Environment Details