mirror of
https://github.com/rubyjs/mini_racer
synced 2023-03-27 23:21:28 -04:00
617 lines
17 KiB
Ruby
617 lines
17 KiB
Ruby
require 'securerandom'
|
|
require 'date'
|
|
require 'test_helper'
|
|
|
|
class MiniRacerTest < Minitest::Test
|
|
# see `test_platform_set_flags_works` below
|
|
MiniRacer::Platform.set_flags! :use_strict
|
|
|
|
def test_segfault
|
|
skip "running this test is very slow"
|
|
# 5000.times do
|
|
# GC.start
|
|
# context = MiniRacer::Context.new(timeout: 5)
|
|
# context.attach("echo", proc{|msg| msg.to_sym.to_s})
|
|
# assert_raises(MiniRacer::EvalError) do
|
|
# context.eval("while(true) echo('foo');")
|
|
# end
|
|
# end
|
|
end
|
|
|
|
def test_that_it_has_a_version_number
|
|
refute_nil ::MiniRacer::VERSION
|
|
end
|
|
|
|
def test_types
|
|
context = MiniRacer::Context.new
|
|
assert_equal 2, context.eval('2')
|
|
assert_equal "two", context.eval('"two"')
|
|
assert_equal 2.1, context.eval('2.1')
|
|
assert_equal true, context.eval('true')
|
|
assert_equal false, context.eval('false')
|
|
assert_equal nil, context.eval('null')
|
|
assert_equal nil, context.eval('undefined')
|
|
end
|
|
|
|
def test_array
|
|
context = MiniRacer::Context.new
|
|
assert_equal [1,"two"], context.eval('[1,"two"]')
|
|
end
|
|
|
|
def test_object
|
|
context = MiniRacer::Context.new
|
|
# remember JavaScript is quirky {"1" : 1} magically turns to {1: 1} cause magic
|
|
assert_equal({"1" => 2, "two" => "two"}, context.eval('var a={"1" : 2, "two" : "two"}; a'))
|
|
end
|
|
|
|
def test_it_returns_runtime_error
|
|
context = MiniRacer::Context.new
|
|
exp = nil
|
|
|
|
begin
|
|
context.eval('var foo=function(){boom;}; foo()')
|
|
rescue => e
|
|
exp = e
|
|
end
|
|
|
|
assert_equal MiniRacer::RuntimeError, exp.class
|
|
|
|
assert_match(/boom/, exp.message)
|
|
assert_match(/foo/, exp.backtrace[0])
|
|
assert_match(/mini_racer/, exp.backtrace[2])
|
|
|
|
# context should not be dead
|
|
assert_equal 2, context.eval('1+1')
|
|
end
|
|
|
|
def test_it_can_stop
|
|
context = MiniRacer::Context.new
|
|
exp = nil
|
|
|
|
begin
|
|
Thread.new do
|
|
sleep 0.001
|
|
context.stop
|
|
end
|
|
context.eval('while(true){}')
|
|
rescue => e
|
|
exp = e
|
|
end
|
|
|
|
assert_equal MiniRacer::ScriptTerminatedError, exp.class
|
|
assert_match(/terminated/, exp.message)
|
|
|
|
end
|
|
|
|
def test_it_can_timeout_during_serialization
|
|
context = MiniRacer::Context.new(timeout: 500)
|
|
|
|
assert_raises(MiniRacer::ScriptTerminatedError) do
|
|
context.eval 'var a = {get a(){ while(true); }}; a'
|
|
end
|
|
end
|
|
|
|
def test_it_can_automatically_time_out_context
|
|
# 2 millisecs is a very short timeout but we don't want test running forever
|
|
context = MiniRacer::Context.new(timeout: 2)
|
|
assert_raises do
|
|
context.eval('while(true){}')
|
|
end
|
|
end
|
|
|
|
def test_returns_javascript_function
|
|
context = MiniRacer::Context.new
|
|
assert_same MiniRacer::JavaScriptFunction, context.eval("var a = function(){}; a").class
|
|
end
|
|
|
|
def test_it_handles_malformed_js
|
|
context = MiniRacer::Context.new
|
|
assert_raises MiniRacer::ParseError do
|
|
context.eval('I am not JavaScript {')
|
|
end
|
|
end
|
|
|
|
def test_it_handles_malformed_js_with_backtrace
|
|
context = MiniRacer::Context.new
|
|
assert_raises MiniRacer::ParseError do
|
|
begin
|
|
context.eval("var i;\ni=2;\nI am not JavaScript {")
|
|
rescue => e
|
|
# I <parse error> am not
|
|
assert_match(/3:2/, e.message)
|
|
raise
|
|
end
|
|
end
|
|
end
|
|
|
|
def test_it_remembers_stuff_in_context
|
|
context = MiniRacer::Context.new
|
|
context.eval('var x = function(){return 22;}')
|
|
assert_equal 22, context.eval('x()')
|
|
end
|
|
|
|
def test_can_attach_functions
|
|
context = MiniRacer::Context.new
|
|
context.eval 'var adder'
|
|
context.attach("adder", proc{|a,b| a+b})
|
|
assert_equal 3, context.eval('adder(1,2)')
|
|
end
|
|
|
|
def test_es6_arrow_functions
|
|
context = MiniRacer::Context.new
|
|
assert_equal 42, context.eval('var adder=(x,y)=>x+y; adder(21,21);')
|
|
end
|
|
|
|
def test_concurrent_access
|
|
context = MiniRacer::Context.new
|
|
context.eval('var counter=0; var plus=()=>counter++;')
|
|
|
|
(1..10).map do
|
|
Thread.new {
|
|
context.eval("plus()")
|
|
}
|
|
end.each(&:join)
|
|
|
|
assert_equal 10, context.eval("counter")
|
|
end
|
|
|
|
class FooError < StandardError
|
|
def initialize(message)
|
|
super(message)
|
|
end
|
|
end
|
|
|
|
def test_attached_exceptions
|
|
context = MiniRacer::Context.new
|
|
context.attach("adder", proc{ raise FooError, "I like foos" })
|
|
assert_raises do
|
|
begin
|
|
raise FooError, "I like foos"
|
|
context.eval('adder()')
|
|
rescue => e
|
|
assert_equal FooError, e.class
|
|
assert_match( /I like foos/, e.message)
|
|
# TODO backtrace splicing so js frames are injected
|
|
raise
|
|
end
|
|
end
|
|
end
|
|
|
|
def test_attached_on_object
|
|
context = MiniRacer::Context.new
|
|
context.eval 'var minion'
|
|
context.attach("minion.speak", proc{"banana"})
|
|
assert_equal "banana", context.eval("minion.speak()")
|
|
end
|
|
|
|
def test_attached_on_nested_object
|
|
context = MiniRacer::Context.new
|
|
context.eval 'var minion'
|
|
context.attach("minion.kevin.speak", proc{"banana"})
|
|
assert_equal "banana", context.eval("minion.kevin.speak()")
|
|
end
|
|
|
|
def test_return_arrays
|
|
context = MiniRacer::Context.new
|
|
context.eval 'var nose'
|
|
context.attach("nose.type", proc{["banana",["nose"]]})
|
|
assert_equal ["banana", ["nose"]], context.eval("nose.type()")
|
|
end
|
|
|
|
def test_return_hash
|
|
context = MiniRacer::Context.new
|
|
context.attach("test", proc{{banana: :nose, "inner" => {42 => 42}}})
|
|
assert_equal({"banana" => "nose", "inner" => {"42" => 42}}, context.eval("test()"))
|
|
end
|
|
|
|
def test_return_date
|
|
context = MiniRacer::Context.new
|
|
test_time = Time.new
|
|
test_datetime = test_time.to_datetime
|
|
context.attach("test", proc{test_time})
|
|
context.attach("test_datetime", proc{test_datetime})
|
|
|
|
# check that marshalling to JS creates a date object (getTime())
|
|
assert_equal((test_time.to_f*1000).to_i, context.eval("var result = test(); result.getTime();").to_i)
|
|
|
|
# check that marshalling to RB creates a Time object
|
|
result = context.eval("test()")
|
|
assert_equal(test_time.class, result.class)
|
|
assert_equal(test_time.tv_sec, result.tv_sec)
|
|
|
|
# check that no precision is lost in the marshalling (js only stores milliseconds)
|
|
assert_equal((test_time.tv_usec/1000.0).floor, (result.tv_usec/1000.0).floor)
|
|
|
|
# check that DateTime gets marshalled to js date and back out as rb Time
|
|
result = context.eval("test_datetime()")
|
|
assert_equal(test_time.class, result.class)
|
|
assert_equal(test_time.tv_sec, result.tv_sec)
|
|
assert_equal((test_time.tv_usec/1000.0).floor, (result.tv_usec/1000.0).floor)
|
|
end
|
|
|
|
def test_datetime_missing
|
|
date_time_backup = Object.send(:remove_const, :DateTime)
|
|
|
|
begin
|
|
# no exceptions should happen here, and non-datetime classes should marshall correctly still.
|
|
context = MiniRacer::Context.new
|
|
test_time = Time.new
|
|
context.attach("test", proc{test_time})
|
|
|
|
assert_equal((test_time.to_f*1000).to_i, context.eval("var result = test(); result.getTime();").to_i)
|
|
|
|
result = context.eval("test()")
|
|
assert_equal(test_time.class, result.class)
|
|
assert_equal(test_time.tv_sec, result.tv_sec)
|
|
assert_equal((test_time.tv_usec/1000.0).floor, (result.tv_usec/1000.0).floor)
|
|
ensure
|
|
Object.const_set(:DateTime, date_time_backup)
|
|
end
|
|
end
|
|
|
|
def test_return_large_number
|
|
context = MiniRacer::Context.new
|
|
test_num = 1_000_000_000_000_000
|
|
context.attach("test", proc{test_num})
|
|
|
|
assert_equal(true, context.eval("test() === 1000000000000000"))
|
|
assert_equal(test_num, context.eval("test()"))
|
|
end
|
|
|
|
def test_return_int_max
|
|
context = MiniRacer::Context.new
|
|
test_num = 2 ** (31) - 1 #last int32 number
|
|
context.attach("test", proc{test_num})
|
|
|
|
assert_equal(true, context.eval("test() === 2147483647"))
|
|
assert_equal(test_num, context.eval("test()"))
|
|
end
|
|
|
|
def test_return_unknown
|
|
context = MiniRacer::Context.new
|
|
test_unknown = Date.new # hits T_DATA in convert_ruby_to_v8
|
|
context.attach("test", proc{test_unknown})
|
|
assert_equal("Undefined Conversion", context.eval("test()"))
|
|
|
|
# clean up and start up a new context
|
|
context = nil
|
|
GC.start
|
|
|
|
context = MiniRacer::Context.new
|
|
test_unknown = Date.new # hits T_DATA in convert_ruby_to_v8
|
|
context.attach("test", proc{test_unknown})
|
|
assert_equal("Undefined Conversion", context.eval("test()"))
|
|
end
|
|
|
|
module Echo
|
|
def self.say(thing)
|
|
thing
|
|
end
|
|
end
|
|
|
|
def test_can_attach_method
|
|
context = MiniRacer::Context.new
|
|
context.eval 'var Echo'
|
|
context.attach("Echo.say", Echo.method(:say))
|
|
assert_equal "hello", context.eval("Echo.say('hello')")
|
|
end
|
|
|
|
def test_attach_error
|
|
context = MiniRacer::Context.new
|
|
context.eval("var minion = 2")
|
|
assert_raises do
|
|
begin
|
|
context.attach("minion.kevin.speak", proc{"banana"})
|
|
rescue => e
|
|
assert_equal MiniRacer::ParseError, e.class
|
|
assert_match(/expecting minion.kevin/, e.message)
|
|
raise
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
def test_load
|
|
context = MiniRacer::Context.new
|
|
context.load(File.dirname(__FILE__) + "/file.js")
|
|
assert_equal "world", context.eval("hello")
|
|
assert_raises do
|
|
context.load(File.dirname(__FILE__) + "/missing.js")
|
|
end
|
|
end
|
|
|
|
def test_contexts_can_be_safely_GCed
|
|
context = MiniRacer::Context.new
|
|
context.eval 'var hello = "world";'
|
|
|
|
context = nil
|
|
GC.start
|
|
end
|
|
|
|
def test_it_can_use_snapshots
|
|
snapshot = MiniRacer::Snapshot.new('function hello() { return "world"; }; var foo = "bar";')
|
|
|
|
context = MiniRacer::Context.new(snapshot: snapshot)
|
|
|
|
assert_equal "world", context.eval("hello()")
|
|
assert_equal "bar", context.eval("foo")
|
|
end
|
|
|
|
def test_snapshot_size
|
|
snapshot = MiniRacer::Snapshot.new('var foo = "bar";')
|
|
|
|
# for some reason sizes seem to change across runs, so we just
|
|
# check it's a positive integer
|
|
assert(snapshot.size > 0)
|
|
end
|
|
|
|
def test_invalid_snapshots_throw_an_exception
|
|
assert_raises(MiniRacer::SnapshotError) do
|
|
MiniRacer::Snapshot.new('var foo = bar;')
|
|
end
|
|
end
|
|
|
|
def test_an_empty_snapshot_is_valid
|
|
MiniRacer::Snapshot.new('')
|
|
MiniRacer::Snapshot.new
|
|
GC.start
|
|
end
|
|
|
|
def test_snapshots_can_be_warmed_up_with_no_side_effects
|
|
# shamelessly insipired by https://github.com/v8/v8/blob/5.3.254/test/cctest/test-serialize.cc#L792-L854
|
|
snapshot_source = <<-JS
|
|
function f() { return Math.sin(1); }
|
|
var a = 5;
|
|
JS
|
|
|
|
snapshot = MiniRacer::Snapshot.new(snapshot_source)
|
|
|
|
warmump_source = <<-JS
|
|
Math.tan(1);
|
|
var a = f();
|
|
Math.sin = 1;
|
|
JS
|
|
|
|
warmed_up_snapshot = snapshot.warmup!(warmump_source)
|
|
|
|
context = MiniRacer::Context.new(snapshot: snapshot)
|
|
|
|
assert_equal 5, context.eval("a")
|
|
assert_equal "function", context.eval("typeof(Math.sin)")
|
|
assert_same snapshot, warmed_up_snapshot
|
|
end
|
|
|
|
def test_invalid_warmup_sources_throw_an_exception
|
|
assert_raises(MiniRacer::SnapshotError) do
|
|
MiniRacer::Snapshot.new('Math.sin = 1;').warmup!('var a = Math.sin(1);')
|
|
end
|
|
end
|
|
|
|
def test_warming_up_with_invalid_source_does_not_affect_the_snapshot_internal_state
|
|
snapshot = MiniRacer::Snapshot.new('Math.sin = 1;')
|
|
|
|
begin
|
|
snapshot.warmup!('var a = Math.sin(1);')
|
|
rescue
|
|
# do nothing
|
|
end
|
|
|
|
context = MiniRacer::Context.new(snapshot: snapshot)
|
|
|
|
assert_equal 1, context.eval('Math.sin')
|
|
end
|
|
|
|
def test_snapshots_can_be_GCed_without_affecting_contexts_created_from_them
|
|
snapshot = MiniRacer::Snapshot.new('Math.sin = 1;')
|
|
context = MiniRacer::Context.new(snapshot: snapshot)
|
|
|
|
# force the snapshot to be GC'ed
|
|
snapshot = nil
|
|
GC.start
|
|
|
|
# the context should still work fine
|
|
assert_equal 1, context.eval('Math.sin')
|
|
end
|
|
|
|
def test_it_can_re_use_isolates_for_multiple_contexts
|
|
snapshot = MiniRacer::Snapshot.new('Math.sin = 1;')
|
|
isolate = MiniRacer::Isolate.new(snapshot)
|
|
|
|
context1 = MiniRacer::Context.new(isolate: isolate)
|
|
assert_equal 1, context1.eval('Math.sin')
|
|
|
|
context1.eval('var a = 5;')
|
|
|
|
context2 = MiniRacer::Context.new(isolate: isolate)
|
|
assert_equal 1, context2.eval('Math.sin')
|
|
assert_raises MiniRacer::RuntimeError do
|
|
begin
|
|
context2.eval('a;')
|
|
rescue => e
|
|
assert_equal('ReferenceError: a is not defined', e.message)
|
|
raise
|
|
end
|
|
end
|
|
|
|
assert_same isolate, context1.isolate
|
|
assert_same isolate, context2.isolate
|
|
end
|
|
|
|
def test_empty_isolate_is_valid_and_can_be_GCed
|
|
MiniRacer::Isolate.new
|
|
GC.start
|
|
end
|
|
|
|
def test_isolates_from_snapshot_dont_get_corrupted_if_the_snapshot_gets_warmed_up_or_GCed
|
|
# basically tests that isolates get their own copy of the snapshot and don't
|
|
# get corrupted if the snapshot is subsequently warmed up
|
|
snapshot_source = <<-JS
|
|
function f() { return Math.sin(1); }
|
|
var a = 5;
|
|
JS
|
|
|
|
snapshot = MiniRacer::Snapshot.new(snapshot_source)
|
|
isolate = MiniRacer::Isolate.new(snapshot)
|
|
|
|
warmump_source = <<-JS
|
|
Math.tan(1);
|
|
var a = f();
|
|
Math.sin = 1;
|
|
JS
|
|
|
|
snapshot.warmup!(warmump_source)
|
|
|
|
context1 = MiniRacer::Context.new(isolate: isolate)
|
|
|
|
assert_equal 5, context1.eval("a")
|
|
assert_equal "function", context1.eval("typeof(Math.sin)")
|
|
|
|
snapshot = nil
|
|
GC.start
|
|
|
|
context2 = MiniRacer::Context.new(isolate: isolate)
|
|
|
|
assert_equal 5, context2.eval("a")
|
|
assert_equal "function", context2.eval("typeof(Math.sin)")
|
|
end
|
|
|
|
def test_isolate_can_be_notified_of_idle_time
|
|
isolate = MiniRacer::Isolate.new
|
|
|
|
assert(isolate.idle_notification(1000))
|
|
end
|
|
|
|
def test_concurrent_access_over_the_same_isolate_1
|
|
isolate = MiniRacer::Isolate.new
|
|
context = MiniRacer::Context.new(isolate: isolate)
|
|
context.eval('var counter=0; var plus=()=>counter++;')
|
|
|
|
(1..10).map do
|
|
Thread.new {
|
|
context.eval("plus()")
|
|
}
|
|
end.each(&:join)
|
|
|
|
assert_equal 10, context.eval('counter')
|
|
end
|
|
|
|
def test_concurrent_access_over_the_same_isolate_2
|
|
isolate = MiniRacer::Isolate.new
|
|
|
|
equals_after_sleep = {}
|
|
|
|
(1..10).map do |i|
|
|
Thread.new {
|
|
random = SecureRandom.hex
|
|
context = MiniRacer::Context.new(isolate: isolate)
|
|
|
|
context.eval('var now = new Date().getTime(); while(new Date().getTime() < now + 20) {}')
|
|
context.eval("var a='#{random}'")
|
|
context.eval('var now = new Date().getTime(); while(new Date().getTime() < now + 20) {}')
|
|
|
|
# cruby hashes are thread safe as long as you don't mess with the
|
|
# same key in different threads
|
|
equals_after_sleep[i] = context.eval('a') == random
|
|
}
|
|
end.each(&:join)
|
|
|
|
assert_equal 10, equals_after_sleep.size
|
|
assert equals_after_sleep.values.all?
|
|
end
|
|
|
|
def test_platform_set_flags_raises_an_exception_if_already_initialized
|
|
# makes sure it's initialized
|
|
MiniRacer::Snapshot.new
|
|
|
|
assert_raises(MiniRacer::PlatformAlreadyInitialized) do
|
|
MiniRacer::Platform.set_flags! :noconcurrent_recompilation
|
|
end
|
|
end
|
|
|
|
def test_platform_set_flags_works
|
|
context = MiniRacer::Context.new
|
|
|
|
assert_raises(MiniRacer::RuntimeError) do
|
|
# should fail because of strict mode set for all these tests
|
|
context.eval 'x = 28'
|
|
end
|
|
end
|
|
|
|
def test_error_on_return_val
|
|
v8 = MiniRacer::Context.new
|
|
assert_raises(MiniRacer::RuntimeError) do
|
|
v8.eval('var o = {}; o.__defineGetter__("bar", function() { return null(); }); o')
|
|
end
|
|
end
|
|
|
|
def test_ruby_based_property_in_rval
|
|
v8 = MiniRacer::Context.new
|
|
v8.attach 'echo', proc{|x| x}
|
|
assert_equal({"bar" => 42}, v8.eval("var o = {get bar() { return echo(42); }}; o"))
|
|
end
|
|
|
|
def test_function_rval
|
|
context = MiniRacer::Context.new
|
|
context.attach("echo", proc{|msg| msg})
|
|
assert_equal("foo", context.eval("echo('foo')"))
|
|
end
|
|
|
|
def test_timeout_in_ruby_land
|
|
context = MiniRacer::Context.new(timeout: 50)
|
|
context.attach('sleep', proc{ sleep 0.1 })
|
|
assert_raises(MiniRacer::ScriptTerminatedError) do
|
|
context.eval('sleep(); "hi";')
|
|
end
|
|
end
|
|
|
|
def test_undef_mem
|
|
context = MiniRacer::Context.new(timeout: 5)
|
|
|
|
context.attach("marsh", proc do |a, b, c|
|
|
return [a,b,c] if a.is_a?(MiniRacer::FailedV8Conversion) || b.is_a?(MiniRacer::FailedV8Conversion) || c.is_a?(MiniRacer::FailedV8Conversion)
|
|
|
|
a[rand(10000).to_s] = "a"
|
|
b[rand(10000).to_s] = "b"
|
|
c[rand(10000).to_s] = "c"
|
|
[a,b,c]
|
|
end)
|
|
|
|
assert_raises do
|
|
# TODO make it raise the correct exception!
|
|
context.eval("var a = [{},{},{}]; while(true) { a = marsh(a[0],a[1],a[2]); }")
|
|
end
|
|
|
|
end
|
|
|
|
class TestPlatform < MiniRacer::Platform
|
|
def self.public_flags_to_strings(flags)
|
|
flags_to_strings(flags)
|
|
end
|
|
end
|
|
|
|
def test_platform_flags_to_strings
|
|
flags = [
|
|
:flag1,
|
|
[[[:flag2]]],
|
|
{key1: :value1},
|
|
{key2: 42,
|
|
key3: 8.7},
|
|
'--i_already_have_leading_hyphens',
|
|
[:'--me_too',
|
|
'i_dont']
|
|
]
|
|
|
|
expected_string_flags = [
|
|
'--flag1',
|
|
'--flag2',
|
|
'--key1 value1',
|
|
'--key2 42',
|
|
'--key3 8.7',
|
|
'--i_already_have_leading_hyphens',
|
|
'--me_too',
|
|
'--i_dont'
|
|
]
|
|
|
|
assert_equal expected_string_flags, TestPlatform.public_flags_to_strings(flags)
|
|
end
|
|
end
|