diff options
Diffstat (limited to 'test/ruby/test_zjit.rb')
| -rw-r--r-- | test/ruby/test_zjit.rb | 837 |
1 files changed, 420 insertions, 417 deletions
diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 5336c6cc47..a56fea6d51 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -9,541 +9,544 @@ require_relative '../lib/jit_support' return unless JITSupport.zjit_supported? class TestZJIT < Test::Unit::TestCase - def test_call_itself - assert_compiles '42', <<~RUBY, call_threshold: 2 - def test = 42.itself - test - test + def test_enabled + assert_runs 'false', <<~RUBY, zjit: false + RubyVM::ZJIT.enabled? + RUBY + assert_runs 'true', <<~RUBY, zjit: true + RubyVM::ZJIT.enabled? RUBY end - def test_nil - assert_compiles 'nil', %q{ - def test = nil - test - } + def test_stats_enabled + assert_runs 'false', <<~RUBY, stats: false + RubyVM::ZJIT.stats_enabled? + RUBY + assert_runs 'true', <<~RUBY, stats: true + RubyVM::ZJIT.stats_enabled? + RUBY end - def test_putobject - assert_compiles '1', %q{ - def test = 1 - 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_leave_param - assert_compiles '5', %q{ - def test(n) = n - test(5) + def test_stats_quiet + # Test that --zjit-stats-quiet collects stats but doesn't print them + script = <<~RUBY + def test = 42 + test + test + puts RubyVM::ZJIT.stats_enabled? + RUBY + + stats_header = "***ZJIT: Printing ZJIT statistics on exit***" + + # 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) + + # 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) + + # 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 + + 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_setlocal - assert_compiles '3', %q{ - def test(n) - m = n - m - end - test(3) - } + 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_send_without_block - assert_compiles '[1, 2, 3]', %q{ - def foo = 1 - def bar(a) = a - 1 - def baz(a, b) = a - b + 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 test1 = foo - def test2 = bar(3) - def test3 = baz(4, 1) + RubyVM::ZJIT.enable - [test1, test2, test3] - } + assert_predicate RubyVM::ZJIT, :enabled? + refute_predicate RubyVM::ZJIT, :stats_enabled? + assert_includes RUBY_DESCRIPTION, "+ZJIT" + RUBY 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_zjit_disable + assert_separately(["--zjit", "--zjit-disable"], <<~'RUBY') + refute_predicate RubyVM::ZJIT, :enabled? + refute_includes RUBY_DESCRIPTION, "+ZJIT" - 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 + RubyVM::ZJIT.enable + + assert_predicate RubyVM::ZJIT, :enabled? + assert_includes RUBY_DESCRIPTION, "+ZJIT" + RUBY 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 + 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 - # 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 + 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? + + RubyVM::ZJIT.enable + + assert_predicate RubyVM::ZJIT, :enabled? + assert_predicate RubyVM::ZJIT, :stats_enabled? + RUBY 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 + 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_opt_mult_overflow - omit 'side exits are not implemented yet' - assert_compiles '[6, -6, 9671406556917033397649408, -9671406556917033397649408, 21267647932558653966460912964485513216]', %q{ - def test(a, b) - a * b + def test_send_exit_with_uninitialized_locals + assert_runs 'nil', %q{ + def entry(init) + function_stub_exit(init) 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) + def function_stub_exit(init) + uninitialized_local = 1 if init + uninitialized_local + end - [r1, r2, r3, r4, r5] - }, call_threshold: 2 + entry(true) # profile and set 1 to the local slot + entry(false) + }, call_threshold: 2, allowed_iseqs: 'entry@-e: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)] - }, call_threshold: 2 + 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_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)] - }, call_threshold: 1 + def test_opt_new_with_custom_allocator_raises + assert_compiles '[42, 42]', %q{ + require "digest" + class C < Digest::Base; end + def test + begin + Digest::Base.new + rescue NotImplementedError + 42 + end + end + [test, test] + }, insns: [:opt_new], call_threshold: 2 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 + def test_uncached_getconstant_path + assert_compiles RUBY_COPYRIGHT.dump, %q{ + def test = RUBY_COPYRIGHT + test + }, call_threshold: 1, insns: [:opt_getconstant_path] 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)] - }, call_threshold: 2 - end + def test_getconstant_path_autoload + # A constant-referencing expression can run arbitrary code through Kernel#autoload. + Dir.mktmpdir('autoload') do |tmpdir| + autoload_path = File.join(tmpdir, 'test_getconstant_path_autoload.rb') + File.write(autoload_path, 'X = RUBY_COPYRIGHT') - 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)] - }, call_threshold: 2 + assert_compiles RUBY_COPYRIGHT.dump, %Q{ + Object.autoload(:X, #{File.realpath(autoload_path).inspect}) + def test = X + test + }, call_threshold: 1, insns: [:opt_getconstant_path] + end 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)] + def test_send_backtrace + backtrace = [ + "-e:2:in 'Object#jit_frame1'", + "-e:3:in 'Object#entry'", + "-e:5:in 'block in <main>'", + "-e:6:in '<main>'", + ] + assert_compiles backtrace.inspect, %q{ + def jit_frame2 = caller # 1 + def jit_frame1 = jit_frame2 # 2 + def entry = jit_frame1 # 3 + entry # profile send # 4 + entry # 5 }, 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)] - }, call_threshold: 2 + # 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 + insn_names = RubyVM::INSTRUCTION_NAMES + zjit, others = insn_names.map.with_index.partition { |name, _| name.start_with?('zjit_') } + zjit_indexes = zjit.map(&:last) + other_indexes = others.map(&:last) + zjit_indexes.product(other_indexes).each do |zjit_index, other_index| + assert zjit_index > other_index, "'#{insn_names[zjit_index]}' at #{zjit_index} "\ + "must be defined after '#{insn_names[other_index]}' at #{other_index}" + end 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)] + def test_require_rubygems + assert_runs 'true', %q{ + require 'rubygems' }, call_threshold: 2 end - def test_new_array_empty - assert_compiles '[]', %q{ - def test = [] - test - } - end - - def test_new_array_nonempty - assert_compiles '[5]', %q{ - def a = 5 - def test = [a] - test - } + 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_new_array_order - assert_compiles '[3, 2, 1]', %q{ - def a = 3 - def b = 2 - def c = 1 - def test = [a, b, c] + def test_stats_availability + assert_runs '[true, true]', %q{ + def test = 1 test - } + [ + RubyVM::ZJIT.stats[:zjit_insn_count] > 0, + RubyVM::ZJIT.stats(:zjit_insn_count) > 0, + ] + }, stats: true end - def test_array_dup - assert_compiles '[1, 2, 3]', %q{ - def test = [1,2,3] - test - } - end + def test_stats_consistency + assert_runs '[]', %q{ + def test = 1 + test # increment some counters - def test_if - assert_compiles '[0, nil]', %q{ - def test(n) - if n < 5 - 0 + 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 - [test(3), test(7)] - } + }, stats: true 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_reset_stats + assert_runs 'true', %q{ + def test = 1 + 100.times { test } - 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 + # Get initial stats and verify they're non-zero + initial_stats = RubyVM::ZJIT.stats + + # Reset the stats + RubyVM::ZJIT.reset_stats! + + # Get stats after reset + reset_stats = RubyVM::ZJIT.stats - 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), - ] + # 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_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_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) + 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 - [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 + # Stay in generated code while enabling tracing + def events.compiled(obj) + String(obj) + @tp.disable; __LINE__ 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 + line = events.compiled(events) + events[0][-1] = (events[0][-1] == line) - 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)] + 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_while_loop - assert_compiles '10', %q{ - def test(n) - i = 0 - while i < n - i = i + 1 - end - i + 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 - 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 + # Stay in generated code while enabling tracing + def events.compiled(obj) + String(obj) + __LINE__ 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 + line = events.compiled(events) + events[0] = (events[0] == line) - 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)] + 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_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 + 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 - [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 tree(depth) + # This duparray is our leaf-gc target. + return [nil, nil] unless depth > 0 - def test_method_call - assert_compiles '12', %q{ - def callee(a, b) - a - b + # Modify the local and pass it to the following calls. + depth -= 1 + [tree(depth), tree(depth)] end def test - callee(4, 2) + 10 + GC.stress = true + 2.times do + t = tree(11) + check(*t) + end + :ok end - test # profile test test - }, call_threshold: 2 + }, call_threshold: 14, num_profiles: 5 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_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 - 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 + # induce an exit just for good measure + array = [] + test(array) + test(array) + def array.itself = :not_itself + test(array) - 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)] - } + 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_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 + 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 - # 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 - insn_names = RubyVM::INSTRUCTION_NAMES - zjit, others = insn_names.map.with_index.partition { |name, _| name.start_with?('zjit_') } - zjit_indexes = zjit.map(&:last) - other_indexes = others.map(&:last) - zjit_indexes.product(other_indexes).each do |zjit_index, other_index| - assert zjit_index > other_index, "'#{insn_names[zjit_index]}' at #{zjit_index} "\ - "must be defined after '#{insn_names[other_index]}' at #{other_index}" - end + 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 # Assert that every method call in `test_script` can be compiled by ZJIT # at a given call_threshold - def assert_compiles(expected, test_script, **opts) + def assert_compiles(expected, test_script, insns: [], **opts) + assert_runs(expected, test_script, insns:, assert_compiles: true, **opts) + end + + # Assert that `test_script` runs successfully with ZJIT enabled. + # Unlike `assert_compiles`, `assert_runs(assert_compiles: false)` + # 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 - _test_proc = -> { - RubyVM::ZJIT.assert_compiles - #{test_script} + 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(#{disasm_method.inspect})).to_a" + end} } - result = _test_proc.call - IO.open(#{pipe_fd}).write(result.inspect) + IO.open(#{pipe_fd}).write(Marshal.dump(result)) RUBY - status, out, err, actual = 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 - - assert_equal expected, actual + 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. " \ + "Make sure to put code directly under the '#{disasm_method}' method." + ) + iseq_insns = iseq.last + + expected_insns = Set.new(insns) + iseq_insns.each do + next unless it.is_a?(Array) + expected_insns.delete(it.first) + end + assert(expected_insns.empty?, -> { "Not present in ISeq: #{expected_insns.to_a}" }) + end end # Run a Ruby process with ZJIT options and a pipe for writing test results - def eval_with_jit(script, call_threshold: 1, num_profiles: 1, timeout: 1000, pipe_fd:, debug: true) - args = [ - "--disable-gems", - "--zjit-call-threshold=#{call_threshold}", - "--zjit-num-profiles=#{num_profiles}", - ] - args << "--zjit-debug" if debug + def eval_with_jit( + script, + call_threshold: 1, + num_profiles: 1, + zjit: true, + stats: false, + debug: true, + allowed_iseqs: nil, + extra_args: nil, + timeout: 1000, + pipe_fd: nil + ) + args = ["--disable-gems", *extra_args] + if zjit + args << "--zjit-call-threshold=#{call_threshold}" + args << "--zjit-num-profiles=#{num_profiles}" + 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") + jitlist.write(allowed_iseqs) + jitlist.close + args << "--zjit-allowed-iseqs=#{jitlist.path}" + 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) pipe_r&.close pipe_w&.close + 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) |
