summaryrefslogtreecommitdiff
path: root/test/ruby/test_yjit_exit_locations.rb
blob: 9bafe392d5ab7cc55febaf926e584132a2b5dd81 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# frozen_string_literal: true
#
# This set of tests can be run with:
# make test-all TESTS='test/ruby/test_yjit_exit_locations.rb' RUN_OPTS="--yjit-call-threshold=1"

require 'test/unit'
require 'envutil'
require 'tmpdir'
require_relative '../lib/jit_support'

return unless defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?

# Tests for YJIT with assertions on tracing exits
# insipired by the MJIT tests in test/ruby/test_yjit.rb
class TestYJITExitLocations < Test::Unit::TestCase
  def test_yjit_trace_exits_and_v_no_error
    _stdout, stderr, _status = EnvUtil.invoke_ruby(%w(-v --yjit-trace-exits), '', true, true)
    refute_includes(stderr, "NoMethodError")
  end

  def test_trace_exits_setclassvariable
    script = 'class Foo; def self.foo; @@foo = 1; end; end; Foo.foo'
    assert_exit_locations(script)
  end

  def test_trace_exits_putobject
    assert_exit_locations('true')
    assert_exit_locations('123')
    assert_exit_locations(':foo')
  end

  def test_trace_exits_opt_not
    assert_exit_locations('!false')
    assert_exit_locations('!nil')
    assert_exit_locations('!true')
    assert_exit_locations('![]')
  end

  private

  def assert_exit_locations(test_script)
    write_results = <<~RUBY
      IO.open(3).write Marshal.dump({
        enabled: RubyVM::YJIT.trace_exit_locations_enabled?,
        exit_locations: RubyVM::YJIT.exit_locations
      })
    RUBY

    script = <<~RUBY
      _test_proc = -> {
        #{test_script}
      }
      result = _test_proc.call
      #{write_results}
    RUBY

    run_script = eval_with_jit(script)
    # If stats are disabled when configuring, --yjit-exit-locations
    # can't be true. We don't want to check if exit_locations hash
    # is not empty because that could indicate a bug in the exit
    # locations collection.
    return unless run_script[:enabled]
    exit_locations = run_script[:exit_locations]

    assert exit_locations.key?(:raw)
    assert exit_locations.key?(:frames)
    assert exit_locations.key?(:lines)
    assert exit_locations.key?(:samples)
    assert exit_locations.key?(:missed_samples)
    assert exit_locations.key?(:gc_samples)

    assert_equal 0, exit_locations[:missed_samples]
    assert_equal 0, exit_locations[:gc_samples]

    assert_not_empty exit_locations[:raw]
    assert_not_empty exit_locations[:frames]
    assert_not_empty exit_locations[:lines]

    exit_locations[:frames].each do |frame_id, frame|
      assert frame.key?(:name)
      assert frame.key?(:file)
      assert frame.key?(:samples)
      assert frame.key?(:total_samples)
      assert frame.key?(:edges)
    end
  end

  def eval_with_jit(script)
    args = [
      "--disable-gems",
      "--yjit-call-threshold=1",
      "--yjit-trace-exits"
    ]
    args << "-e" << script_shell_encode(script)
    stats_r, stats_w = IO.pipe
    _out, _err, _status = EnvUtil.invoke_ruby(args,
                                              '', true, true, timeout: 1000, ios: { 3 => stats_w }
                                             )
    stats_w.close
    stats = stats_r.read
    stats = Marshal.load(stats) if !stats.empty?
    stats_r.close
    stats
  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
  end
end