diff options
Diffstat (limited to 'test/ruby/test_zjit.rb')
| -rw-r--r-- | test/ruby/test_zjit.rb | 2011 |
1 files changed, 276 insertions, 1735 deletions
diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 8f83e858a6..a56fea6d51 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -27,206 +27,125 @@ class TestZJIT < Test::Unit::TestCase RUBY end - def test_enable_through_env - child_env = {'RUBY_YJIT_ENABLE' => nil, 'RUBY_ZJIT_ENABLE' => '1'} - assert_in_out_err([child_env, '-v'], '') do |stdout, stderr| - assert_includes(stdout.first, '+ZJIT') - assert_equal([], stderr) - end - end - - def test_call_itself - assert_compiles '42', <<~RUBY, call_threshold: 2 - def test = 42.itself - test - test + def test_stats_string_no_zjit + assert_runs 'nil', <<~RUBY, zjit: false + RubyVM::ZJIT.stats_string + RUBY + assert_runs 'true', <<~RUBY, stats: false + RubyVM::ZJIT.stats_string.is_a?(String) + RUBY + assert_runs 'true', <<~RUBY, stats: true + RubyVM::ZJIT.stats_string.is_a?(String) RUBY end - def test_nil - assert_compiles 'nil', %q{ - def test = nil - test - } - end - - def test_putobject - assert_compiles '1', %q{ - def test = 1 - test - } - end - - def test_putstring - assert_compiles '""', %q{ - def test = "#{""}" - test - }, insns: [:putstring] - end - - def test_putchilldedstring - assert_compiles '""', %q{ - def test = "" + def test_stats_quiet + # Test that --zjit-stats-quiet collects stats but doesn't print them + script = <<~RUBY + def test = 42 test - }, insns: [:putchilledstring] - end - - def test_leave_param - assert_compiles '5', %q{ - def test(n) = n - test(5) - } - end - - def test_setglobal - assert_compiles '1', %q{ - def test - $a = 1 - $a - end - test - }, insns: [:setglobal] - end + puts RubyVM::ZJIT.stats_enabled? + RUBY - def test_string_intern - assert_compiles ':foo123', %q{ - def test - :"foo#{123}" - end + stats_header = "***ZJIT: Printing ZJIT statistics on exit***" - test - }, insns: [:intern] - end + # With --zjit-stats, stats should be printed to stderr + out, err, status = eval_with_jit(script, stats: true) + assert_success(out, err, status) + assert_includes(err, stats_header) + assert_equal("true\n", out) - def test_setglobal_with_trace_var_exception - assert_compiles '"rescued"', %q{ - def test - $a = 1 - rescue - "rescued" - end - - trace_var(:$a) { raise } - test - }, insns: [:setglobal] - end + # With --zjit-stats-quiet, stats should NOT be printed but still enabled + out, err, status = eval_with_jit(script, stats: :quiet) + assert_success(out, err, status) + refute_includes(err, stats_header) + assert_equal("true\n", out) - def test_setlocal - assert_compiles '3', %q{ - def test(n) - m = n - m - end - test(3) - } - end + # With --zjit-stats=<path>, stats should be printed to the path + Tempfile.create("zjit-stats-") {|tmp| + stats_file = tmp.path + tmp.puts("Lorem ipsum dolor sit amet, consectetur adipiscing elit, ...") + tmp.close - def test_setlocal_on_eval - assert_compiles '1', %q{ - @b = binding - eval('a = 1', @b) - eval('a', @b) + out, err, status = eval_with_jit(script, stats: stats_file) + assert_success(out, err, status) + refute_includes(err, stats_header) + assert_equal("true\n", out) + assert_equal stats_header, File.open(stats_file) {|f| f.gets(chomp: true)}, "should be overwritten" } end - def test_call_a_forwardable_method - assert_runs '[]', %q{ - def test_root = forwardable - def forwardable(...) = Array.[](...) - test_root - test_root - }, call_threshold: 2 + def test_enable_through_env + child_env = {'RUBY_YJIT_ENABLE' => nil, 'RUBY_ZJIT_ENABLE' => '1'} + assert_in_out_err([child_env, '-v'], '') do |stdout, stderr| + assert_includes(stdout.first, '+ZJIT') + assert_equal([], stderr) + end end - def test_setlocal_on_eval_with_spill - assert_compiles '1', %q{ - @b = binding - eval('a = 1; itself', @b) - eval('a', @b) - } - end + def test_zjit_enable + # --disable-all is important in case the build/environment has YJIT enabled by + # default through e.g. -DYJIT_FORCE_ENABLE. Can't enable ZJIT when YJIT is on. + assert_separately(["--disable-all"], <<~'RUBY') + refute_predicate RubyVM::ZJIT, :enabled? + refute_predicate RubyVM::ZJIT, :stats_enabled? + refute_includes RUBY_DESCRIPTION, "+ZJIT" - def test_nested_local_access - assert_compiles '[1, 2, 3]', %q{ - 1.times do |l2| - 1.times do |l1| - define_method(:test) do - l1 = 1 - l2 = 2 - l3 = 3 - [l1, l2, l3] - end - end - end + RubyVM::ZJIT.enable - test - test - test - }, call_threshold: 3, insns: [:getlocal, :setlocal, :getlocal_WC_0, :setlocal_WC_1] + assert_predicate RubyVM::ZJIT, :enabled? + refute_predicate RubyVM::ZJIT, :stats_enabled? + assert_includes RUBY_DESCRIPTION, "+ZJIT" + RUBY end - def test_read_local_written_by_children_iseqs - omit "This test fails right now because Send doesn't compile." - - assert_compiles '[1, 2]', %q{ - def test - l1 = nil - l2 = nil - tap do |_| - l1 = 1 - tap do |_| - l2 = 2 - end - end + def test_zjit_disable + assert_separately(["--zjit", "--zjit-disable"], <<~'RUBY') + refute_predicate RubyVM::ZJIT, :enabled? + refute_includes RUBY_DESCRIPTION, "+ZJIT" - [l1, l2] - end + RubyVM::ZJIT.enable - test - test - }, call_threshold: 2 + assert_predicate RubyVM::ZJIT, :enabled? + assert_includes RUBY_DESCRIPTION, "+ZJIT" + RUBY end - def test_send_without_block - assert_compiles '[1, 2, 3]', %q{ - def foo = 1 - def bar(a) = a - 1 - def baz(a, b) = a - b - - def test1 = foo - def test2 = bar(3) - def test3 = baz(4, 1) - - [test1, test2, test3] - } + def test_zjit_prelude_kernel_prepend + # Simulate what bundler/setup can do: prepend a module to Kernel during + # the prelude via the BUNDLER_SETUP mechanism in rubygems.rb: + # require ENV["BUNDLER_SETUP"] if ENV["BUNDLER_SETUP"] && !defined?(Bundler) + Tempfile.create(["kernel_prepend", ".rb"]) do |f| + f.write("Kernel.prepend(Module.new)\n") + f.flush + assert_separately([{ "BUNDLER_SETUP" => f.path }, "--enable=gems", "--zjit"], "", ignore_stderr: true) + end end - def test_send_with_six_args - assert_compiles '[1, 2, 3, 4, 5, 6]', %q{ - def foo(a1, a2, a3, a4, a5, a6) - [a1, a2, a3, a4, a5, a6] - end + def test_zjit_enable_respects_existing_options + assert_separately(['--zjit-disable', '--zjit-stats-quiet'], <<~RUBY) + refute_predicate RubyVM::ZJIT, :enabled? + assert_predicate RubyVM::ZJIT, :stats_enabled? - def test - foo(1, 2, 3, 4, 5, 6) - end + RubyVM::ZJIT.enable - test # profile send - test - }, call_threshold: 2 + assert_predicate RubyVM::ZJIT, :enabled? + assert_predicate RubyVM::ZJIT, :stats_enabled? + RUBY end - def test_send_on_heap_object_in_spilled_arg - # This leads to a register spill, so not using `assert_compiles` - assert_runs 'Hash', %q{ - def entry(a1, a2, a3, a4, a5, a6, a7, a8, a9) - a9.itself.class - end - - entry(1, 2, 3, 4, 5, 6, 7, 8, {}) # profile - entry(1, 2, 3, 4, 5, 6, 7, 8, {}) - }, call_threshold: 2 + def test_toplevel_binding + # Not using assert_compiles, which doesn't use the toplevel frame for `test_script`. + out, err, status = eval_with_jit(%q{ + a = 1 + b = 2 + TOPLEVEL_BINDING.local_variable_set(:b, 3) + c = 4 + print [a, b, c] + }) + assert_success(out, err, status) + assert_equal "[1, 3, 4]", out end def test_send_exit_with_uninitialized_locals @@ -245,889 +164,27 @@ class TestZJIT < Test::Unit::TestCase }, call_threshold: 2, allowed_iseqs: 'entry@-e:2' end - def test_invokebuiltin - omit 'Test fails at the moment due to not handling optional parameters' - assert_compiles '["."]', %q{ - def test = Dir.glob(".") - test - } - end - - def test_invokebuiltin_delegate - assert_compiles '[[], true]', %q{ - def test = [].clone(freeze: true) - r = test - [r, r.frozen?] - } - end - - def test_opt_plus_const - assert_compiles '3', %q{ - def test = 1 + 2 - test # profile opt_plus - test - }, call_threshold: 2 - end - - def test_opt_plus_fixnum - assert_compiles '3', %q{ - def test(a, b) = a + b - test(0, 1) # profile opt_plus - test(1, 2) - }, call_threshold: 2 - end - - def test_opt_plus_chain - assert_compiles '6', %q{ - def test(a, b, c) = a + b + c - test(0, 1, 2) # profile opt_plus - test(1, 2, 3) - }, call_threshold: 2 - end - - def test_opt_plus_left_imm - assert_compiles '3', %q{ - def test(a) = 1 + a - test(1) # profile opt_plus - test(2) - }, call_threshold: 2 - end - - def test_opt_plus_type_guard_exit - assert_compiles '[3, 3.0]', %q{ - def test(a) = 1 + a - test(1) # profile opt_plus - [test(2), test(2.0)] - }, call_threshold: 2 - end - - def test_opt_plus_type_guard_exit_with_locals - assert_compiles '[6, 6.0]', %q{ - def test(a) - local = 3 - 1 + a + local - end - test(1) # profile opt_plus - [test(2), test(2.0)] - }, call_threshold: 2 - end - - def test_opt_plus_type_guard_nested_exit - assert_compiles '[4, 4.0]', %q{ - def side_exit(n) = 1 + n - def jit_frame(n) = 1 + side_exit(n) - def entry(n) = jit_frame(n) - entry(2) # profile send - [entry(2), entry(2.0)] - }, call_threshold: 2 - end - - def test_opt_plus_type_guard_nested_exit_with_locals - assert_compiles '[9, 9.0]', %q{ - def side_exit(n) - local = 2 - 1 + n + local - end - def jit_frame(n) - local = 3 - 1 + side_exit(n) + local - end - def entry(n) = jit_frame(n) - entry(2) # profile send - [entry(2), entry(2.0)] - }, call_threshold: 2 - end - - # Test argument ordering - def test_opt_minus - assert_compiles '2', %q{ - def test(a, b) = a - b - test(2, 1) # profile opt_minus - test(6, 4) - }, call_threshold: 2 - end - - def test_opt_mult - assert_compiles '6', %q{ - def test(a, b) = a * b - test(1, 2) # profile opt_mult - test(2, 3) - }, call_threshold: 2 - end - - def test_opt_mult_overflow - assert_compiles '[6, -6, 9671406556917033397649408, -9671406556917033397649408, 21267647932558653966460912964485513216]', %q{ - def test(a, b) - a * b - end - test(1, 1) # profile opt_mult - - r1 = test(2, 3) - r2 = test(2, -3) - r3 = test(2 << 40, 2 << 41) - r4 = test(2 << 40, -2 << 41) - r5 = test(1 << 62, 1 << 62) - - [r1, r2, r3, r4, r5] - }, call_threshold: 2 - end - - def test_opt_eq - assert_compiles '[true, false]', %q{ - def test(a, b) = a == b - test(0, 2) # profile opt_eq - [test(1, 1), test(0, 1)] - }, insns: [:opt_eq], call_threshold: 2 - end - - def test_opt_eq_with_minus_one - assert_compiles '[false, true]', %q{ - def test(a) = a == -1 - test(1) # profile opt_eq - [test(0), test(-1)] - }, insns: [:opt_eq], call_threshold: 2 - end - - def test_opt_neq_dynamic - # TODO(max): Don't split this test; instead, run all tests with and without - # profiling. - assert_compiles '[false, true]', %q{ - def test(a, b) = a != b - test(0, 2) # profile opt_neq - [test(1, 1), test(0, 1)] - }, insns: [:opt_neq], call_threshold: 1 - end - - def test_opt_neq_fixnum - assert_compiles '[false, true]', %q{ - def test(a, b) = a != b - test(0, 2) # profile opt_neq - [test(1, 1), test(0, 1)] - }, call_threshold: 2 - end - - def test_opt_lt - assert_compiles '[true, false, false]', %q{ - def test(a, b) = a < b - test(2, 3) # profile opt_lt - [test(0, 1), test(0, 0), test(1, 0)] - }, insns: [:opt_lt], call_threshold: 2 - end - - def test_opt_lt_with_literal_lhs - assert_compiles '[false, false, true]', %q{ - def test(n) = 2 < n - test(2) # profile opt_lt - [test(1), test(2), test(3)] - }, insns: [:opt_lt], call_threshold: 2 - end - - def test_opt_le - assert_compiles '[true, true, false]', %q{ - def test(a, b) = a <= b - test(2, 3) # profile opt_le - [test(0, 1), test(0, 0), test(1, 0)] - }, insns: [:opt_le], call_threshold: 2 - end - - def test_opt_gt - assert_compiles '[false, false, true]', %q{ - def test(a, b) = a > b - test(2, 3) # profile opt_gt - [test(0, 1), test(0, 0), test(1, 0)] - }, insns: [:opt_gt], call_threshold: 2 - end - - def test_opt_empty_p - assert_compiles('[false, false, true]', <<~RUBY, insns: [:opt_empty_p]) - def test(x) = x.empty? - return test([1]), test("1"), test({}) - RUBY - end - - def test_opt_succ - assert_compiles('[0, "B"]', <<~RUBY, insns: [:opt_succ]) - def test(obj) = obj.succ - return test(-1), test("A") - RUBY - end - - def test_opt_and - assert_compiles('[1, [3, 2, 1]]', <<~RUBY, insns: [:opt_and]) - def test(x, y) = x & y - return test(0b1101, 3), test([3, 2, 1, 4], [8, 1, 2, 3]) - RUBY - end - - def test_opt_or - assert_compiles('[11, [3, 2, 1]]', <<~RUBY, insns: [:opt_or]) - def test(x, y) = x | y - return test(0b1000, 3), test([3, 2, 1], [1, 2, 3]) - RUBY - end - - def test_fixnum_and - assert_compiles '1', %q{ - def test(a, b) = a & b - test(2, 2) - test(2, 2) - test(5, 3) - }, call_threshold: 2, insns: [:opt_and] - end - - def test_fixnum_and_side_exit - assert_compiles 'false', %q{ - def test(a, b) = a & b - test(2, 2) - test(2, 2) - test(true, false) - }, call_threshold: 2, insns: [:opt_and] - end - - def test_fixnum_or - assert_compiles '3', %q{ - def test(a, b) = a | b - test(5, 3) - test(5, 3) - test(1, 2) - }, call_threshold: 2, insns: [:opt_or] - end - - def test_fixnum_or_side_exit - assert_compiles 'true', %q{ - def test(a, b) = a | b - test(2, 2) - test(2, 2) - test(true, false) - }, call_threshold: 2, insns: [:opt_or] - end - - def test_fixnum_mul - assert_compiles '12', %q{ - C = 3 - def test(n) = C * n - test(4) - test(4) - test(4) - }, call_threshold: 2, insns: [:opt_mult] - end - - def test_opt_not - assert_compiles('[true, true, false]', <<~RUBY, insns: [:opt_not]) - def test(obj) = !obj - return test(nil), test(false), test(0) - RUBY - end - - def test_opt_regexpmatch2 - assert_compiles('[1, nil]', <<~RUBY, insns: [:opt_regexpmatch2]) - def test(haystack) = /needle/ =~ haystack - return test("kneedle"), test("") - RUBY - end - - def test_opt_ge - assert_compiles '[false, true, true]', %q{ - def test(a, b) = a >= b - test(2, 3) # profile opt_ge - [test(0, 1), test(0, 0), test(1, 0)] - }, insns: [:opt_ge], call_threshold: 2 - end - - def test_new_hash_empty - assert_compiles '{}', %q{ - def test = {} - test - }, insns: [:newhash] - end - - def test_new_hash_nonempty - assert_compiles '{"key" => "value", 42 => 100}', %q{ - def test - key = "key" - value = "value" - num = 42 - result = 100 - {key => value, num => result} - end - test - }, insns: [:newhash] - end - - def test_new_hash_single_key_value - assert_compiles '{"key" => "value"}', %q{ - def test = {"key" => "value"} - test - }, insns: [:newhash] - end - - def test_new_hash_with_computation - assert_compiles '{"sum" => 5, "product" => 6}', %q{ - def test(a, b) - {"sum" => a + b, "product" => a * b} - end - test(2, 3) - }, insns: [:newhash] - end - - def test_new_hash_with_user_defined_hash_method - assert_runs 'true', %q{ - class CustomKey - attr_reader :val - - def initialize(val) - @val = val - end - - def hash - @val.hash - end - - def eql?(other) - other.is_a?(CustomKey) && @val == other.val - end - end - - def test - key = CustomKey.new("key") - hash = {key => "value"} - hash[key] == "value" - end - test - } + def test_opt_new_with_custom_allocator + assert_compiles '"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"', %q{ + require "digest" + def test = Digest::SHA256.new.hexdigest + test; test + }, insns: [:opt_new], call_threshold: 2 end - def test_new_hash_with_user_hash_method_exception - assert_runs 'RuntimeError', %q{ - class BadKey - def hash - raise "Hash method failed!" - end - end - + def test_opt_new_with_custom_allocator_raises + assert_compiles '[42, 42]', %q{ + require "digest" + class C < Digest::Base; end def test - key = BadKey.new - {key => "value"} - end - - begin - test - rescue => e - e.class - end - } - end - - def test_new_hash_with_user_eql_method_exception - assert_runs 'RuntimeError', %q{ - class BadKey - def hash + begin + Digest::Base.new + rescue NotImplementedError 42 end - - def eql?(other) - raise "Eql method failed!" - end - end - - def test - key1 = BadKey.new - key2 = BadKey.new - {key1 => "value1", key2 => "value2"} - end - - begin - test - rescue => e - e.class - end - } - end - - def test_opt_hash_freeze - assert_compiles '{}', <<~RUBY, insns: [:opt_hash_freeze] - def test = {}.freeze - test - RUBY - end - - def test_opt_ary_freeze - assert_compiles '[]', <<~RUBY, insns: [:opt_ary_freeze] - def test = [].freeze - test - RUBY - end - - def test_opt_str_freeze - assert_compiles '""', <<~RUBY, insns: [:opt_str_freeze] - def test = "".freeze - test - RUBY - end - - def test_opt_str_uminus - assert_compiles '""', <<~RUBY, insns: [:opt_str_uminus] - def test = -"" - test - RUBY - end - - def test_new_array_empty - assert_compiles '[]', %q{ - def test = [] - test - }, insns: [:newarray] - end - - def test_new_array_nonempty - assert_compiles '[5]', %q{ - def a = 5 - def test = [a] - test - } - end - - def test_new_array_order - assert_compiles '[3, 2, 1]', %q{ - def a = 3 - def b = 2 - def c = 1 - def test = [a, b, c] - test - } - end - - def test_array_dup - assert_compiles '[1, 2, 3]', %q{ - def test = [1,2,3] - test - } - end - - def test_new_range_inclusive - assert_compiles '1..5', %q{ - def test(a, b) = a..b - test(1, 5) - } - end - - def test_new_range_exclusive - assert_compiles '1...5', %q{ - def test(a, b) = a...b - test(1, 5) - } - end - - def test_new_range_with_literal - assert_compiles '3..10', %q{ - def test(n) = n..10 - test(3) - } - end - - def test_if - assert_compiles '[0, nil]', %q{ - def test(n) - if n < 5 - 0 - end - end - [test(3), test(7)] - } - end - - def test_if_else - assert_compiles '[0, 1]', %q{ - def test(n) - if n < 5 - 0 - else - 1 - end - end - [test(3), test(7)] - } - end - - def test_if_else_params - assert_compiles '[1, 20]', %q{ - def test(n, a, b) - if n < 5 - a - else - b - end - end - [test(3, 1, 2), test(7, 10, 20)] - } - end - - def test_if_else_nested - assert_compiles '[3, 8, 9, 14]', %q{ - def test(a, b, c, d, e) - if 2 < a - if a < 4 - b - else - c - end - else - if a < 0 - d - else - e - end - end - end - [ - test(-1, 1, 2, 3, 4), - test( 0, 5, 6, 7, 8), - test( 3, 9, 10, 11, 12), - test( 5, 13, 14, 15, 16), - ] - } - end - - def test_if_else_chained - assert_compiles '[12, 11, 21]', %q{ - def test(a) - (if 2 < a then 1 else 2 end) + (if a < 4 then 10 else 20 end) - end - [test(0), test(3), test(5)] - } - end - - def test_if_elsif_else - assert_compiles '[0, 2, 1]', %q{ - def test(n) - if n < 5 - 0 - elsif 8 < n - 1 - else - 2 - end - end - [test(3), test(7), test(9)] - } - end - - def test_ternary_operator - assert_compiles '[1, 20]', %q{ - def test(n, a, b) - n < 5 ? a : b - end - [test(3, 1, 2), test(7, 10, 20)] - } - end - - def test_ternary_operator_nested - assert_compiles '[2, 21]', %q{ - def test(n, a, b) - (n < 5 ? a : b) + 1 - end - [test(3, 1, 2), test(7, 10, 20)] - } - end - - def test_while_loop - assert_compiles '10', %q{ - def test(n) - i = 0 - while i < n - i = i + 1 - end - i - end - test(10) - } - end - - def test_while_loop_chain - assert_compiles '[135, 270]', %q{ - def test(n) - i = 0 - while i < n - i = i + 1 - end - while i < n * 10 - i = i * 3 - end - i - end - [test(5), test(10)] - } - end - - def test_while_loop_nested - assert_compiles '[0, 4, 12]', %q{ - def test(n, m) - i = 0 - while i < n - j = 0 - while j < m - j += 2 - end - i += j - end - i - end - [test(0, 0), test(1, 3), test(10, 5)] - } - end - - def test_while_loop_if_else - assert_compiles '[9, -1]', %q{ - def test(n) - i = 0 - while i < n - if n >= 10 - return -1 - else - i = i + 1 - end - end - i - end - [test(9), test(10)] - } - end - - def test_if_while_loop - assert_compiles '[9, 12]', %q{ - def test(n) - i = 0 - if n < 10 - while i < n - i += 1 - end - else - while i < n - i += 3 - end - end - i - end - [test(9), test(10)] - } - end - - def test_live_reg_past_ccall - assert_compiles '2', %q{ - def callee = 1 - def test = callee + callee - test - } - end - - def test_method_call - assert_compiles '12', %q{ - def callee(a, b) - a - b - end - - def test - callee(4, 2) + 10 - end - - test # profile test - test - }, call_threshold: 2 - end - - def test_recursive_fact - assert_compiles '[1, 6, 720]', %q{ - def fact(n) - if n == 0 - return 1 - end - return n * fact(n-1) - end - [fact(0), fact(3), fact(6)] - } - end - - def test_profiled_fact - assert_compiles '[1, 6, 720]', %q{ - def fact(n) - if n == 0 - return 1 - end - return n * fact(n-1) - end - fact(1) # profile fact - [fact(0), fact(3), fact(6)] - }, call_threshold: 3, num_profiles: 2 - end - - def test_recursive_fib - assert_compiles '[0, 2, 3]', %q{ - def fib(n) - if n < 2 - return n - end - return fib(n-1) + fib(n-2) - end - [fib(0), fib(3), fib(4)] - } - end - - def test_profiled_fib - assert_compiles '[0, 2, 3]', %q{ - def fib(n) - if n < 2 - return n - end - return fib(n-1) + fib(n-2) - end - fib(3) # profile fib - [fib(0), fib(3), fib(4)] - }, call_threshold: 5, num_profiles: 3 - end - - def test_spilled_basic_block_args - assert_compiles '55', %q{ - def test(n1, n2) - n3 = 3 - n4 = 4 - n5 = 5 - n6 = 6 - n7 = 7 - n8 = 8 - n9 = 9 - n10 = 10 - if n1 < n2 - n1 + n2 + n3 + n4 + n5 + n6 + n7 + n8 + n9 + n10 - end - end - test(1, 2) - } - end - - def test_spilled_method_args - assert_runs '55', %q{ - def foo(n1, n2, n3, n4, n5, n6, n7, n8, n9, n10) - n1 + n2 + n3 + n4 + n5 + n6 + n7 + n8 + n9 + n10 - end - - def test - foo(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) - end - - test - } - - # TODO(Shopify/ruby#716): Support spills and change to assert_compiles - assert_runs '1', %q{ - def a(n1,n2,n3,n4,n5,n6,n7,n8,n9) = n1+n9 - a(2,0,0,0,0,0,0,0,-1) - } - - # TODO(Shopify/ruby#716): Support spills and change to assert_compiles - assert_runs '0', %q{ - def a(n1,n2,n3,n4,n5,n6,n7,n8) = n8 - a(1,1,1,1,1,1,1,0) - } - - # TODO(Shopify/ruby#716): Support spills and change to assert_compiles - # self param with spilled param - assert_runs '"main"', %q{ - def a(n1,n2,n3,n4,n5,n6,n7,n8) = self - a(1,0,0,0,0,0,0,0).to_s - } - end - - def test_spilled_param_new_arary - # TODO(Shopify/ruby#716): Support spills and change to assert_compiles - assert_runs '[:ok]', %q{ - def a(n1,n2,n3,n4,n5,n6,n7,n8) = [n8] - a(0,0,0,0,0,0,0, :ok) - } - end - - def test_forty_param_method - # This used to a trigger a miscomp on A64 due - # to a memory displacement larger than 9 bits. - assert_compiles '1', %Q{ - def foo(#{'_,' * 39} n40) = n40 - - foo(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1) - } - end - - - def test_opt_aref_with - assert_compiles ':ok', %q{ - def aref_with(hash) = hash["key"] - - aref_with({ "key" => :ok }) - } - end - - def test_putself - assert_compiles '3', %q{ - class Integer - def minus(a) - self - a - end - end - 5.minus(2) - } - end - - def test_getinstancevariable - assert_compiles 'nil', %q{ - def test() = @foo - - test() - } - assert_compiles '3', %q{ - @foo = 3 - def test() = @foo - - test() - } - end - - def test_setinstancevariable - assert_compiles '1', %q{ - def test() = @foo = 1 - - test() - @foo - } - end - - def test_attr_reader - assert_compiles '[4, 4]', %q{ - class C - attr_reader :foo - - def initialize - @foo = 4 - end - end - - def test(c) = c.foo - c = C.new - [test(c), test(c)] - }, call_threshold: 2, insns: [:opt_send_without_block] - end - - def test_attr_accessor - assert_compiles '[4, 4]', %q{ - class C - attr_accessor :foo - - def initialize - @foo = 4 - end end - - def test(c) = c.foo - c = C.new - [test(c), test(c)] - }, call_threshold: 2, insns: [:opt_send_without_block] + [test, test] + }, insns: [:opt_new], call_threshold: 2 end def test_uncached_getconstant_path @@ -1151,78 +208,6 @@ class TestZJIT < Test::Unit::TestCase end end - def test_constant_invalidation - assert_compiles '123', <<~RUBY, call_threshold: 2, insns: [:opt_getconstant_path] - class C; end - def test = C - test - test - - C = 123 - test - RUBY - end - - def test_constant_path_invalidation - assert_compiles '["Foo::C", "Foo::C", "Bar::C"]', <<~RUBY, call_threshold: 2, insns: [:opt_getconstant_path] - module A - module B; end - end - - module Foo - C = "Foo::C" - end - - module Bar - C = "Bar::C" - end - - A::B = Foo - - def test = A::B::C - - result = [] - - result << test - result << test - - A::B = Bar - - result << test - result - RUBY - end - - def test_single_ractor_mode_invalidation - # Without invalidating the single-ractor mode, the test would crash - assert_compiles '"errored but not crashed"', <<~RUBY, call_threshold: 2, insns: [:opt_getconstant_path] - C = Object.new - - def test - C - rescue Ractor::IsolationError - "errored but not crashed" - end - - test - test - - Ractor.new { - test - }.value - RUBY - end - - def test_dupn - assert_compiles '[[1], [1, 1], :rhs, [nil, :rhs]]', <<~RUBY, insns: [:dupn] - def test(array) = (array[1, 2] ||= :rhs) - - one = [1, 1] - start_empty = [] - [test(one), one, test(start_empty), start_empty] - RUBY - end - def test_send_backtrace backtrace = [ "-e:2:in 'Object#jit_frame1'", @@ -1239,235 +224,6 @@ class TestZJIT < Test::Unit::TestCase }, call_threshold: 2 end - def test_bop_invalidation - omit 'Invalidation on BOP redefinition is not implemented yet' - assert_compiles '', %q{ - def test - eval(<<~RUBY) - class Integer - def +(_) = 100 - end - RUBY - 1 + 2 - end - test - } - end - - def test_defined_with_defined_values - assert_compiles '["constant", "method", "global-variable"]', %q{ - class Foo; end - def bar; end - $ruby = 1 - - def test = return defined?(Foo), defined?(bar), defined?($ruby) - - test - }, insns: [:defined] - end - - def test_defined_with_undefined_values - assert_compiles '[nil, nil, nil]', %q{ - def test = return defined?(Foo), defined?(bar), defined?($ruby) - - test - }, insns: [:defined] - end - - def test_defined_with_method_call - assert_compiles '["method", nil]', %q{ - def test = return defined?("x".reverse(1)), defined?("x".reverse(1).reverse) - - test - }, insns: [:defined] - end - - def test_defined_yield - assert_compiles "nil", "defined?(yield)" - assert_compiles '[nil, nil, "yield"]', %q{ - def test = defined?(yield) - [test, test, test{}] - }, call_threshold: 2, insns: [:defined] - end - - def test_defined_yield_from_block - # This will do some EP hopping to find the local EP, - # so it's slightly different than doing it outside of a block. - - omit 'Test fails at the moment due to missing Send codegen' - - assert_compiles '[nil, nil, "yield"]', %q{ - def test - yield_self { yield_self { defined?(yield) } } - end - - [test, test, test{}] - }, call_threshold: 2, insns: [:defined] - end - - def test_invokeblock_without_block_after_jit_call - assert_compiles '"no block given (yield)"', %q{ - def test(*arr, &b) - arr.class - yield - end - begin - test - rescue => e - e.message - end - } - end - - def test_putspecialobject_vm_core_and_cbase - assert_compiles '10', %q{ - def test - alias bar test - 10 - end - - test - bar - }, insns: [:putspecialobject] - end - - def test_putspecialobject_const_base - assert_compiles '1', %q{ - Foo = 1 - - def test = Foo - - # First call: populates the constant cache - test - # Second call: triggers ZJIT compilation with warm cache - # RubyVM::ZJIT.assert_compiles will panic if this fails to compile - test - }, call_threshold: 2 - end - - def test_branchnil - assert_compiles '[2, nil]', %q{ - def test(x) - x&.succ - end - [test(1), test(nil)] - }, call_threshold: 1, insns: [:branchnil] - end - - def test_nil_nil - assert_compiles 'true', %q{ - def test = nil.nil? - test - }, insns: [:opt_nil_p] - end - - def test_non_nil_nil - assert_compiles 'false', %q{ - def test = 1.nil? - test - }, insns: [:opt_nil_p] - end - - def test_getspecial_last_match - assert_compiles '"hello"', %q{ - def test(str) - str =~ /hello/ - $& - end - test("hello world") - }, insns: [:getspecial] - end - - def test_getspecial_match_pre - assert_compiles '"hello "', %q{ - def test(str) - str =~ /world/ - $` - end - test("hello world") - }, insns: [:getspecial] - end - - def test_getspecial_match_post - assert_compiles '" world"', %q{ - def test(str) - str =~ /hello/ - $' - end - test("hello world") - }, insns: [:getspecial] - end - - def test_getspecial_match_last_group - assert_compiles '"world"', %q{ - def test(str) - str =~ /(hello) (world)/ - $+ - end - test("hello world") - }, insns: [:getspecial] - end - - def test_getspecial_numbered_match_1 - assert_compiles '"hello"', %q{ - def test(str) - str =~ /(hello) (world)/ - $1 - end - test("hello world") - }, insns: [:getspecial] - end - - def test_getspecial_numbered_match_2 - assert_compiles '"world"', %q{ - def test(str) - str =~ /(hello) (world)/ - $2 - end - test("hello world") - }, insns: [:getspecial] - end - - def test_getspecial_numbered_match_nonexistent - assert_compiles 'nil', %q{ - def test(str) - str =~ /(hello)/ - $2 - end - test("hello world") - }, insns: [:getspecial] - end - - def test_getspecial_no_match - assert_compiles 'nil', %q{ - def test(str) - str =~ /xyz/ - $& - end - test("hello world") - }, insns: [:getspecial] - end - - def test_getspecial_complex_pattern - assert_compiles '"123"', %q{ - def test(str) - str =~ /(\d+)/ - $1 - end - test("abc123def") - }, insns: [:getspecial] - end - - def test_getspecial_multiple_groups - assert_compiles '"456"', %q{ - def test(str) - str =~ /(\d+)-(\d+)/ - $2 - end - test("123-456") - }, insns: [:getspecial] - end - # tool/ruby_vm/views/*.erb relies on the zjit instructions a) being contiguous and # b) being reliably ordered after all the other instructions. def test_instruction_order @@ -1488,429 +244,190 @@ class TestZJIT < Test::Unit::TestCase end def test_require_rubygems_with_auto_compact + omit("GC.auto_compact= support is required for this test") unless GC.respond_to?(:auto_compact=) assert_runs 'true', %q{ GC.auto_compact = true require 'rubygems' }, call_threshold: 2 end - def test_stats + def test_stats_availability assert_runs '[true, true]', %q{ def test = 1 test [ - RubyVM::ZJIT.stats[:zjit_insns_count] > 0, - RubyVM::ZJIT.stats(:zjit_insns_count) > 0, + RubyVM::ZJIT.stats[:zjit_insn_count] > 0, + RubyVM::ZJIT.stats(:zjit_insn_count) > 0, ] }, stats: true end - def test_zjit_option_uses_array_each_in_ruby - omit 'ZJIT wrongly compiles Array#each, so it is disabled for now' - assert_runs '"<internal:array>"', %q{ - Array.instance_method(:each).source_location&.first - } - end - - def test_profile_under_nested_jit_call - assert_compiles '[nil, nil, 3]', %q{ - def profile - 1 + 2 - end + def test_stats_consistency + assert_runs '[]', %q{ + def test = 1 + test # increment some counters - def jit_call(flag) - if flag - profile + RubyVM::ZJIT.stats.to_a.filter_map do |key, value| + # The value may be incremented, but the class should stay the same + other_value = RubyVM::ZJIT.stats(key) + if value.class != other_value.class + [key, value, other_value] end end - - def entry(flag) - jit_call(flag) - end - - [entry(false), entry(false), entry(true)] - }, call_threshold: 2 - end - - def test_bop_redefinition - assert_runs '[3, :+, 100]', %q{ - def test - 1 + 2 - end - - test # profile opt_plus - [test, Integer.class_eval { def +(_) = 100 }, test] - }, call_threshold: 2 - end - - def test_bop_redefinition_with_adjacent_patch_points - assert_runs '[15, :+, 100]', %q{ - def test - 1 + 2 + 3 + 4 + 5 - end - - test # profile opt_plus - [test, Integer.class_eval { def +(_) = 100 }, test] - }, call_threshold: 2 + }, stats: true end - # ZJIT currently only generates a MethodRedefined patch point when the method - # is called on the top-level self. - def test_method_redefinition_with_top_self - assert_runs '["original", "redefined"]', %q{ - def foo - "original" - end - - def test = foo - - test; test - - result1 = test - - # Redefine the method - def foo - "redefined" - end + def test_reset_stats + assert_runs 'true', %q{ + def test = 1 + 100.times { test } - result2 = test + # Get initial stats and verify they're non-zero + initial_stats = RubyVM::ZJIT.stats - [result1, result2] - }, call_threshold: 2 - end + # Reset the stats + RubyVM::ZJIT.reset_stats! - def test_module_name_with_guard_passes - assert_compiles '"Integer"', %q{ - def test(mod) - mod.name - end + # Get stats after reset + reset_stats = RubyVM::ZJIT.stats - test(String) - test(Integer) - }, call_threshold: 2 + [ + # After reset, counters should be zero or at least much smaller + # (some instructions might execute between reset and reading stats) + :zjit_insn_count.then { |s| initial_stats[s] > 0 && reset_stats[s] < initial_stats[s] }, + :compiled_iseq_count.then { |s| initial_stats[s] > 0 && reset_stats[s] < initial_stats[s] } + ].all? + }, stats: true end - def test_module_name_with_guard_side_exit - # This test demonstrates that the guard side exit works correctly - # In this case, when we call with a non-Class object, it should fall back to interpreter - assert_compiles '["String", "Integer", "Bar"]', %q{ - class MyClass - def name = "Bar" - end - - def test(mod) - mod.name - end - - results = [] - results << test(String) - results << test(Integer) - results << test(MyClass.new) - - results - }, call_threshold: 2 + def test_zjit_option_uses_array_each_in_ruby + omit 'ZJIT wrongly compiles Array#each, so it is disabled for now' + assert_runs '"<internal:array>"', %q{ + Array.instance_method(:each).source_location&.first + } end - def test_objtostring_calls_to_s_on_non_strings - assert_compiles '["foo", "foo"]', %q{ - results = [] - - class Foo - def to_s - "foo" - end + def test_line_tracepoint_on_c_method + assert_compiles '"[[:line, true]]"', %q{ + events = [] + events.instance_variable_set( + :@tp, + TracePoint.new(:line) { |tp| events << [tp.event, tp.lineno] if tp.path == __FILE__ } + ) + def events.to_str + @tp.enable; '' end - def test(str) - "#{str}" + # Stay in generated code while enabling tracing + def events.compiled(obj) + String(obj) + @tp.disable; __LINE__ end - results << test(Foo.new) - results << test(Foo.new) + line = events.compiled(events) + events[0][-1] = (events[0][-1] == line) - results + events.to_s # can't dump events as it's a singleton object AND it has a TracePoint instance variable, which also can't be dumped } end - def test_objtostring_rewrite_does_not_call_to_s_on_strings - assert_compiles '["foo", "foo"]', %q{ - results = [] - - class String - def to_s - "bad" - end + def test_targeted_line_tracepoint_in_c_method_call + assert_compiles '"[true]"', %q{ + events = [] + events.instance_variable_set(:@tp, TracePoint.new(:line) { |tp| events << tp.lineno }) + def events.to_str + @tp.enable(target: method(:compiled)) + '' end - def test(foo) - "#{foo}" + # Stay in generated code while enabling tracing + def events.compiled(obj) + String(obj) + __LINE__ end - results << test("foo") - results << test("foo") + line = events.compiled(events) + events[0] = (events[0] == line) - results + events.to_s # can't dump events as it's a singleton object AND it has a TracePoint instance variable, which also can't be dumped } end - def test_objtostring_rewrite_does_not_call_to_s_on_string_subclasses - assert_compiles '["foo", "foo"]', %q{ - results = [] - - class StringSubclass < String - def to_s - "bad" - end + def test_regression_cfp_sp_set_correctly_before_leaf_gc_call + assert_compiles ':ok', %q{ + def check(l, r) + return 1 unless l + 1 + check(*l) + check(*r) end - foo = StringSubclass.new("foo") + def tree(depth) + # This duparray is our leaf-gc target. + return [nil, nil] unless depth > 0 - def test(str) - "#{str}" + # Modify the local and pass it to the following calls. + depth -= 1 + [tree(depth), tree(depth)] end - results << test(foo) - results << test(foo) - - results - } - end - - def test_string_bytesize_with_guard - assert_compiles '5', %q{ - def test(str) - str.bytesize + def test + GC.stress = true + 2.times do + t = tree(11) + check(*t) + end + :ok end - test('hello') - test('world') - }, call_threshold: 2 - end - - def test_nil_value_nil_opt_with_guard - assert_compiles 'true', %q{ - def test(val) = val.nil? - - test(nil) - test(nil) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_nil_value_nil_opt_with_guard_side_exit - assert_compiles 'false', %q{ - def test(val) = val.nil? - - test(nil) - test(nil) - test(1) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_true_nil_opt_with_guard - assert_compiles 'false', %q{ - def test(val) = val.nil? - - test(true) - test(true) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_true_nil_opt_with_guard_side_exit - assert_compiles 'true', %q{ - def test(val) = val.nil? - - test(true) - test(true) - test(nil) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_false_nil_opt_with_guard - assert_compiles 'false', %q{ - def test(val) = val.nil? - - test(false) - test(false) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_false_nil_opt_with_guard_side_exit - assert_compiles 'true', %q{ - def test(val) = val.nil? - - test(false) - test(false) - test(nil) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_integer_nil_opt_with_guard - assert_compiles 'false', %q{ - def test(val) = val.nil? - - test(1) - test(2) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_integer_nil_opt_with_guard_side_exit - assert_compiles 'true', %q{ - def test(val) = val.nil? - - test(1) - test(2) - test(nil) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_float_nil_opt_with_guard - assert_compiles 'false', %q{ - def test(val) = val.nil? - - test(1.0) - test(2.0) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_float_nil_opt_with_guard_side_exit - assert_compiles 'true', %q{ - def test(val) = val.nil? - - test(1.0) - test(2.0) - test(nil) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_symbol_nil_opt_with_guard - assert_compiles 'false', %q{ - def test(val) = val.nil? - - test(:foo) - test(:bar) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_symbol_nil_opt_with_guard_side_exit - assert_compiles 'true', %q{ - def test(val) = val.nil? - - test(:foo) - test(:bar) - test(nil) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_class_nil_opt_with_guard - assert_compiles 'false', %q{ - def test(val) = val.nil? - - test(String) - test(Integer) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_class_nil_opt_with_guard_side_exit - assert_compiles 'true', %q{ - def test(val) = val.nil? - - test(String) - test(Integer) - test(nil) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_module_nil_opt_with_guard - assert_compiles 'false', %q{ - def test(val) = val.nil? - - test(Enumerable) - test(Kernel) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_module_nil_opt_with_guard_side_exit - assert_compiles 'true', %q{ - def test(val) = val.nil? - - test(Enumerable) - test(Kernel) - test(nil) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_basic_object_guard_works_with_immediate - assert_compiles 'NilClass', %q{ - class Foo; end - - def test(val) = val.class - - test(Foo.new) - test(Foo.new) - test(nil) - }, call_threshold: 2 - end - - def test_basic_object_guard_works_with_false - assert_compiles 'FalseClass', %q{ - class Foo; end - - def test(val) = val.class - - test(Foo.new) - test(Foo.new) - test(false) - }, call_threshold: 2 - end - - def test_string_concat - assert_compiles '"123"', %q{ - def test = "#{1}#{2}#{3}" - test - }, insns: [:concatstrings] + }, call_threshold: 14, num_profiles: 5 end - def test_string_concat_empty - assert_compiles '""', %q{ - def test = "#{}" + def test_exit_tracing + # Smoke test: --zjit-trace-exits writes a Fuchsia trace (.fxt) file to /tmp + assert_compiles('true', <<~RUBY, extra_args: ['--zjit-trace-exits']) + def test(object) = object.itself - test - }, insns: [:concatstrings] - end - - def test_regexp_interpolation - assert_compiles '/123/', %q{ - def test = /#{1}#{2}#{3}/ + # induce an exit just for good measure + array = [] + test(array) + test(array) + def array.itself = :not_itself + test(array) - test - }, insns: [:toregexp] + fxt_files = Dir.glob("/tmp/perfetto-\#{Process.pid}.fxt") + result = fxt_files.length == 1 && !File.empty?(fxt_files.first) + File.unlink(*fxt_files) + result + RUBY end - def test_new_range_non_leaf - assert_compiles '(0/1)..1', %q{ - def jit_entry(v) = make_range_then_exit(v) - - def make_range_then_exit(v) - range = (v..1) - super rescue range # TODO(alan): replace super with side-exit intrinsic - end - - jit_entry(0) # profile - jit_entry(0) # compile - jit_entry(0/1r) # run without stub - }, call_threshold: 2 + def test_send_no_profiles_with_disabled_specialized_instruction + # Regression test: when specialized_instruction is disabled (as power_assert does), + # eval'd code uses `send` instead of `opt_send_without_block`, producing SendNoProfiles. + # The `times` call with a literal block is the SendNoProfiles send whose exit profiling + # triggers recompilation of `run`. After recompilation, `make`'s eval("proc { }") crashes + # in vm_make_env_each because the caller frame's EP[-1] (specval) has a stale value. + assert_runs ':ok', <<~RUBY + RubyVM::InstructionSequence.compile_option = { specialized_instruction: false } + eval <<~'INNERRUBY' + def make = eval("proc { }") + def run(n) = n.times { make } + INNERRUBY + run(6) + :ok + RUBY end - def test_raise_in_second_argument - assert_compiles '{ok: true}', %q{ - def write(hash, key) - hash[key] = raise rescue true - hash - end - - write({}, :ok) - } + def test_float_arithmetic + assert_compiles '4.0', 'def test = 1.5 + 2.5; test' + assert_compiles '6.0', 'def test = 2.0 * 3.0; test' + assert_compiles '1.5', 'def test = 3.5 - 2.0; test' + assert_compiles '2.5', 'def test = 5.0 / 2.0; test' + assert_compiles '4.5', 'def test = 1.5 * 3; test' # Float * Fixnum + assert_compiles 'true', 'def test = (Float::NAN + 1.0).nan?; test' + assert_compiles 'Infinity', 'def test = Float::INFINITY * 2.0; test' + assert_compiles '3', 'def test = 3.7.to_i; test' + assert_compiles '-2', 'def test = (-2.9).to_i; test' end private @@ -1926,31 +443,33 @@ class TestZJIT < Test::Unit::TestCase # allows ZJIT to skip compiling methods. def assert_runs(expected, test_script, insns: [], assert_compiles: false, **opts) pipe_fd = 3 + disasm_method = :test script = <<~RUBY ret_val = (_test_proc = -> { #{('RubyVM::ZJIT.assert_compiles; ' if assert_compiles)}#{test_script.lstrip} }).call result = { ret_val:, #{ unless insns.empty? - 'insns: RubyVM::InstructionSequence.of(method(:test)).to_a' + "insns: RubyVM::InstructionSequence.of(method(#{disasm_method.inspect})).to_a" end} } IO.open(#{pipe_fd}).write(Marshal.dump(result)) RUBY - status, out, err, result = eval_with_jit(script, pipe_fd:, **opts) - - message = "exited with status #{status.to_i}" - message << "\nstdout:\n```\n#{out}```\n" unless out.empty? - message << "\nstderr:\n```\n#{err}```\n" unless err.empty? - assert status.success?, message + out, err, status, result = eval_with_jit(script, pipe_fd:, **opts) + assert_success(out, err, status) result = Marshal.load(result) assert_equal(expected, result.fetch(:ret_val).inspect) unless insns.empty? iseq = result.fetch(:insns) - assert_equal("YARVInstructionSequence/SimpleDataFormat", iseq.first, "failed to get iseq disassembly") + assert_equal( + "YARVInstructionSequence/SimpleDataFormat", + iseq.first, + "Failed to get ISEQ disassembly. " \ + "Make sure to put code directly under the '#{disasm_method}' method." + ) iseq_insns = iseq.last expected_insns = Set.new(insns) @@ -1971,14 +490,22 @@ class TestZJIT < Test::Unit::TestCase stats: false, debug: true, allowed_iseqs: nil, + extra_args: nil, timeout: 1000, - pipe_fd: + pipe_fd: nil ) - args = ["--disable-gems"] + args = ["--disable-gems", *extra_args] if zjit args << "--zjit-call-threshold=#{call_threshold}" args << "--zjit-num-profiles=#{num_profiles}" - args << "--zjit-stats" if stats + case stats + when true + args << "--zjit-stats" + when :quiet + args << "--zjit-stats-quiet" + else + args << "--zjit-stats=#{stats}" if stats + end args << "--zjit-debug" if debug if allowed_iseqs jitlist = Tempfile.new("jitlist") @@ -1988,18 +515,25 @@ class TestZJIT < Test::Unit::TestCase end end args << "-e" << script_shell_encode(script) - pipe_r, pipe_w = IO.pipe - # Separate thread so we don't deadlock when - # the child ruby blocks writing the output to pipe_fd - pipe_out = nil - pipe_reader = Thread.new do - pipe_out = pipe_r.read - pipe_r.close + ios = {} + if pipe_fd + pipe_r, pipe_w = IO.pipe + # Separate thread so we don't deadlock when + # the child ruby blocks writing the output to pipe_fd + pipe_out = nil + pipe_reader = Thread.new do + pipe_out = pipe_r.read + pipe_r.close + end + ios[pipe_fd] = pipe_w end - out, err, status = EnvUtil.invoke_ruby(args, '', true, true, rubybin: RbConfig.ruby, timeout: timeout, ios: { pipe_fd => pipe_w }) - pipe_w.close - pipe_reader.join(timeout) - [status, out, err, pipe_out] + result = EnvUtil.invoke_ruby(args, '', true, true, rubybin: RbConfig.ruby, timeout: timeout, ios:) + if pipe_fd + pipe_w.close + pipe_reader.join(timeout) + result << pipe_out + end + result ensure pipe_reader&.kill pipe_reader&.join(timeout) @@ -2008,6 +542,13 @@ class TestZJIT < Test::Unit::TestCase jitlist&.unlink end + def assert_success(out, err, status) + message = "exited with status #{status.to_i}" + message << "\nstdout:\n```\n#{out}```\n" unless out.empty? + message << "\nstderr:\n```\n#{err}```\n" unless err.empty? + assert status.success?, message + end + def script_shell_encode(s) # We can't pass utf-8-encoded characters directly in a shell arg. But we can use Ruby \u constants. s.chars.map { |c| c.ascii_only? ? c : "\\u%x" % c.codepoints[0] }.join |
