1
0
Fork 0
mirror of https://github.com/rubyjs/mini_racer synced 2023-03-27 23:21:28 -04:00
mini_racer/test/mini_racer_test.rb
Sam Saffron 5f29361ae9
FEATURE: add support for Isolate#low_memory_notification
This is another mechanism we can use for releasing as much memory as
possible

Note: Isolate#idle_notification can be used as well, but it is much slower
to act.

low_memory_notification forces a full GC and will clear up large amounts of
space from the V8 heap.

This also adds support for `ensure_gc_after_idle` options for
MiniRacer::Context this allows you to automatically conserve memory on
contexts, and only runs the GC when the context was idle for a certain
amount of time.
2020-05-15 12:05:14 +10:00

869 lines
24 KiB
Ruby

# frozen_string_literal: true
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_nil context.eval('null')
assert_nil context.eval('undefined')
end
def test_compile_nil_context
context = MiniRacer::Context.new
assert_raises(ArgumentError) do
assert_equal 2, context.eval(nil)
end
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
def test_max_memory
context = MiniRacer::Context.new(max_memory: 200_000_000)
assert_raises(MiniRacer::V8OutOfMemoryError) { context.eval('let s = 1000; var a = new Array(s); a.fill(0); while(true) {s *= 1.1; let n = new Array(Math.floor(s)); n.fill(0); a = a.concat(n); };') }
end
def test_max_memory_for_call
context = MiniRacer::Context.new(max_memory: 200_000_000)
context.eval(<<~JS)
let s;
function memory_test() {
var a = new Array(s);
a.fill(0);
while(true) {
s *= 1.1;
let n = new Array(Math.floor(s));
n.fill(0);
a = a.concat(n);
if (s > 1000000) {
return;
}
}
}
function set_s(val) {
s = val;
}
JS
context.call('set_s', 1000)
assert_raises(MiniRacer::V8OutOfMemoryError) { context.call('memory_test') }
s = context.eval('s')
assert_operator(s, :>, 100_000)
end
def test_negative_max_memory
context = MiniRacer::Context.new(max_memory: -200_000_000)
assert_nil(context.instance_variable_get(:@max_memory))
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_snapshot_dump
snapshot = MiniRacer::Snapshot.new('var foo = "bar";')
dump = snapshot.dump
assert_equal(String, dump.class)
assert_equal(Encoding::ASCII_8BIT, dump.encoding)
assert_equal(snapshot.size, dump.length)
end
def test_invalid_snapshots_throw_an_exception
begin
MiniRacer::Snapshot.new('var foo = bar;')
rescue MiniRacer::SnapshotError => e
assert(e.backtrace[0].include? 'JavaScript')
got_error = true
end
assert(got_error, "should raise")
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)
warmup_source = <<-JS
Math.tan(1);
var a = f();
Math.sin = 1;
JS
warmed_up_snapshot = snapshot.warmup!(warmup_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_invalid_warmup_sources_throw_an_exception
assert_raises(ArgumentError) do
MiniRacer::Snapshot.new('function f() { return 1 }').warmup!([])
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
# returns true if embedder should stop calling
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
def test_can_dispose_context
context = MiniRacer::Context.new(timeout: 5)
context.dispose
assert_raises(MiniRacer::ContextDisposedError) do
context.eval("a")
end
end
def test_estimated_size
context = MiniRacer::Context.new(timeout: 5)
context.eval("let a='testing';")
stats = context.heap_stats
# eg: {:total_physical_size=>1280640, :total_heap_size_executable=>4194304, :total_heap_size=>3100672, :used_heap_size=>1205376, :heap_size_limit=>1501560832}
assert_equal(
[:total_physical_size, :total_heap_size_executable, :total_heap_size, :used_heap_size, :heap_size_limit].sort,
stats.keys.sort
)
assert(stats.values.all?{|v| v > 0}, "expecting the isolate to have values for all the vals")
end
def test_releasing_memory
context = MiniRacer::Context.new
context.isolate.low_memory_notification
start_heap = context.heap_stats[:used_heap_size]
context.eval("'#{"x" * 1_000_000}'")
context.isolate.low_memory_notification
end_heap = context.heap_stats[:used_heap_size]
assert((end_heap - start_heap).abs < 1000, "expecting most of the 1_000_000 long string to be freed")
end
def test_ensure_gc
context = MiniRacer::Context.new(ensure_gc_after_idle: 0.001)
context.isolate.low_memory_notification
start_heap = context.heap_stats[:used_heap_size]
context.eval("'#{"x" * 10_000_000}'")
sleep 0.005
end_heap = context.heap_stats[:used_heap_size]
assert((end_heap - start_heap).abs < 1000, "expecting most of the 1_000_000 long string to be freed")
end
def test_eval_with_filename
context = MiniRacer::Context.new()
context.eval("var foo = function(){baz();}", filename: 'b/c/foo1.js')
got_error = false
begin
context.eval("foo()", filename: 'baz1.js')
rescue MiniRacer::RuntimeError => e
assert_match(/foo1.js/, e.backtrace[0])
assert_match(/baz1.js/, e.backtrace[1])
got_error = true
end
assert(got_error, "should raise")
end
def test_estimated_size_when_disposed
context = MiniRacer::Context.new(timeout: 5)
context.eval("let a='testing';")
context.dispose
stats = context.heap_stats
assert(stats.values.all?{|v| v==0}, "should have 0 values once disposed")
end
def test_can_dispose
skip "takes too long"
#
# junk_it_up
# 3.times do
# GC.start(full_mark: true, immediate_sweep: true)
# end
end
def junk_it_up
1000.times do
context = MiniRacer::Context.new(timeout: 5)
context.dispose
end
end
def test_attached_recursion
context = MiniRacer::Context.new(timeout: 20)
context.attach("a", proc{|a| a})
context.attach("b", proc{|a| a})
context.eval('const obj = {get r(){ b() }}; a(obj);')
end
def test_no_disposal_of_isolate_when_it_is_referenced
isolate = MiniRacer::Isolate.new
context = MiniRacer::Context.new(isolate: isolate)
context.dispose
_context2 = MiniRacer::Context.new(isolate: isolate) # Received signal 11 SEGV_MAPERR
end
def test_context_starts_with_no_isolate_value
context = MiniRacer::Context.new
assert_equal context.instance_variable_get('@isolate'), false
end
def test_context_isolate_value_is_kept
context = MiniRacer::Context.new
isolate = context.isolate
assert_same isolate, context.isolate
end
def test_isolate_is_nil_after_disposal
context = MiniRacer::Context.new
context.dispose
assert_nil context.isolate
context = MiniRacer::Context.new
context.isolate
context.dispose
assert_nil context.isolate
end
def test_heap_dump
f = Tempfile.new("heap")
path = f.path
f.unlink
context = MiniRacer::Context.new
context.eval('let x = 1000;')
context.write_heap_snapshot(path)
dump = File.read(path)
assert dump.length > 0
FileUtils.rm(path)
end
def test_pipe_leak
# in Ruby 2.7 pipes will stay open for longer
# make sure that we clean up early so pipe file
# descriptors are not kept around
context = MiniRacer::Context.new(timeout: 1000)
10000.times do |i|
context.eval("'hello'")
end
end
def test_symbol_support
context = MiniRacer::Context.new()
assert_equal :foo, context.eval("Symbol('foo')")
end
def test_proxy_support
js = <<~JS
function MyProxy(reference) {
return new Proxy(function() {}, {
get: function(obj, prop) {
return new MyProxy(reference.concat(prop));
},
apply: function(target, thisArg, argumentsList) {
myFunctionLogger(reference);
}
});
};
(new MyProxy([])).function_call(new MyProxy([])-1)
JS
context = MiniRacer::Context.new()
context.attach('myFunctionLogger', ->(property) { })
context.eval(js)
end
end