diff options
Diffstat (limited to 'test/dtrace')
| -rw-r--r-- | test/dtrace/dummy.rb | 2 | ||||
| -rw-r--r-- | test/dtrace/helper.rb | 161 | ||||
| -rw-r--r-- | test/dtrace/test_array_create.rb | 36 | ||||
| -rw-r--r-- | test/dtrace/test_cmethod.rb | 50 | ||||
| -rw-r--r-- | test/dtrace/test_function_entry.rb | 89 | ||||
| -rw-r--r-- | test/dtrace/test_gc.rb | 27 | ||||
| -rw-r--r-- | test/dtrace/test_hash_create.rb | 53 | ||||
| -rw-r--r-- | test/dtrace/test_load.rb | 53 | ||||
| -rw-r--r-- | test/dtrace/test_method_cache.rb | 29 | ||||
| -rw-r--r-- | test/dtrace/test_object_create_start.rb | 36 | ||||
| -rw-r--r-- | test/dtrace/test_raise.rb | 30 | ||||
| -rw-r--r-- | test/dtrace/test_require.rb | 41 | ||||
| -rw-r--r-- | test/dtrace/test_singleton_function.rb | 57 | ||||
| -rw-r--r-- | test/dtrace/test_string.rb | 28 |
14 files changed, 692 insertions, 0 deletions
diff --git a/test/dtrace/dummy.rb b/test/dtrace/dummy.rb new file mode 100644 index 0000000000..932cb4e625 --- /dev/null +++ b/test/dtrace/dummy.rb @@ -0,0 +1,2 @@ +# frozen_string_literal: false +# this is a dummy file used by test/dtrace/test_require.rb diff --git a/test/dtrace/helper.rb b/test/dtrace/helper.rb new file mode 100644 index 0000000000..9e8c7ecd52 --- /dev/null +++ b/test/dtrace/helper.rb @@ -0,0 +1,161 @@ +# -*- coding: us-ascii -*- +# frozen_string_literal: false +require 'test/unit' +require 'tempfile' + +if Process.euid == 0 + ok = true +elsif (sudo = ENV["SUDO"]) and !sudo.empty? and (`#{sudo} echo ok` rescue false) + ok = true +else + ok = false +end + +impl = :dtrace + +# GNU/Linux distros with Systemtap support allows unprivileged users +# in the stapusr and statdev groups to work. +if RUBY_PLATFORM =~ /linux/ + impl = :stap + begin + require 'etc' + ok = (%w[stapusr stapdev].map {|g|(Etc.getgrnam(g) || raise(ArgumentError)).gid} & Process.groups).size == 2 + rescue LoadError, ArgumentError + end unless ok +end + +if ok + case RUBY_PLATFORM + when /darwin/i + begin + require 'pty' + rescue LoadError + end + end +end + +# use miniruby to reduce the amount of trace data we don't care about +rubybin = "miniruby#{RbConfig::CONFIG["EXEEXT"]}" +rubybin = File.join(File.dirname(EnvUtil.rubybin), rubybin) +rubybin = EnvUtil.rubybin unless File.executable?(rubybin) + +# make sure ruby was built with --enable-dtrace and we can run +# dtrace(1) or stap(1): +cmd = "#{rubybin} --disable=gems -eexit" +case impl +when :dtrace; cmd = %W(dtrace -l -n ruby$target:::gc-sweep-end -c #{cmd}) +when :stap; cmd = %W(stap -l process.mark("gc__sweep__end") -c #{cmd}) +else + warn "don't know how to check if built with #{impl} support" + cmd = false +end + +NEEDED_ENVS = [RbConfig::CONFIG["LIBPATHENV"], "RUBY", "RUBYOPT"].compact + +if cmd and ok + sudocmd = [] + if sudo + sudocmd << sudo + NEEDED_ENVS.each {|name| val = ENV[name] and sudocmd << "#{name}=#{val}"} + end + ok = system(*sudocmd, *cmd, err: IO::NULL, out: IO::NULL) +end + +module DTrace + class TestCase < Test::Unit::TestCase + INCLUDE = File.expand_path('..', File.dirname(__FILE__)) + + if RUBY_PLATFORM =~ /darwin/i + READ_PROBES = proc do |cmd| + lines = nil + PTY.spawn(*cmd) do |io, _, pid| + lines = io.readlines.each {|line| line.sub!(/\r$/, "")} + Process.wait(pid) + end + lines + end if defined?(PTY) + end + + # only handles simple cases, use a Hash for d_program + # if there are more complex cases + def dtrace2systemtap(d_program) + translate = lambda do |str| + # dtrace starts args with '0', systemtap with '1' and prefixes '$' + str = str.gsub(/\barg(\d+)/) { "$arg#{$1.to_i + 1}" } + # simple function mappings: + str.gsub!(/\bcopyinstr\b/, 'user_string') + str.gsub!(/\bstrstr\b/, 'isinstr') + str + end + out = '' + cond = nil + d_program.split(/^/).each do |l| + case l + when /\bruby\$target:::([a-z-]+)/ + name = $1.gsub(/-/, '__') + out << %Q{probe process.mark("#{name}")\n} + when %r{/(.+)/} + cond = translate.call($1) + when "{\n" + out << l + out << "if (#{cond}) {\n" if cond + when "}\n" + out << "}\n" if cond + out << l + else + out << translate.call(l) + end + end + out + end + + DTRACE_CMD ||= %w[dtrace] + + READ_PROBES ||= proc do |cmd| + IO.popen(cmd, err: [:child, :out], &:readlines) + end + + def trap_probe d_program, ruby_program + if Hash === d_program + d_program = d_program[IMPL] or + omit "#{d_program} not implemented for #{IMPL}" + elsif String === d_program && IMPL == :stap + d_program = dtrace2systemtap(d_program) + end + d = Tempfile.new(%w'probe .d') + d.write d_program + d.flush + + rb = Tempfile.new(%w'probed .rb') + rb.write ruby_program + rb.flush + + d_path = d.path + rb_path = rb.path + cmd = "#{RUBYBIN} --disable=gems -I#{INCLUDE} #{rb_path}" + if IMPL == :stap + cmd = %W(stap #{d_path} -c #{cmd}) + else + cmd = [*DTRACE_CMD, "-q", "-s", d_path, "-c", cmd ] + end + if sudo = @@sudo + NEEDED_ENVS.each do |name| + if val = ENV[name] + cmd.unshift("#{name}=#{val}") + end + end + cmd.unshift(sudo) + end + probes = READ_PROBES.(cmd) + d.close(true) + rb.close(true) + yield(d_path, rb_path, probes) + end + end +end if ok + +if ok + DTrace::TestCase.class_variable_set(:@@sudo, sudo) + DTrace::TestCase.const_set(:IMPL, impl) + DTrace::TestCase.const_set(:RUBYBIN, rubybin) +end diff --git a/test/dtrace/test_array_create.rb b/test/dtrace/test_array_create.rb new file mode 100644 index 0000000000..1bf20085ba --- /dev/null +++ b/test/dtrace/test_array_create.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: false +require_relative 'helper' + +module DTrace + class TestArrayCreate < TestCase + def test_lit + trap_probe(probe, '[]') { |_,rbfile,saw| + saw = saw.map(&:split).find_all { |num, file, line| + file == rbfile && num == '0' + } + assert_equal([rbfile], saw.map { |line| line[1] }) + assert_equal(['1'], saw.map { |line| line[2] }) + } + end + + def test_many_lit + trap_probe(probe, '[1,2,3,4]') { |_,rbfile,orig| + saw = orig.map(&:split).find_all { |num, file, line| + file == rbfile && num == '4' && line == '1' + } + assert_operator saw.length, :>, 0, orig + } + end + + private + def probe type = 'array' + <<-eoprobe +ruby$target:::#{type}-create +/arg1 && arg2/ +{ + printf("%d %s %d\\n", arg0, copyinstr(arg1), arg2); +} + eoprobe + end + end +end if defined?(DTrace::TestCase) diff --git a/test/dtrace/test_cmethod.rb b/test/dtrace/test_cmethod.rb new file mode 100644 index 0000000000..a0aa1b646b --- /dev/null +++ b/test/dtrace/test_cmethod.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: false +require_relative 'helper' + +module DTrace + class TestCMethod < TestCase + def test_entry + probe = <<-eoprobe +ruby$target:::cmethod-entry +{ + printf("%s %s %s %d\\n", copyinstr(arg0), copyinstr(arg1), copyinstr(arg2), arg3); +} + eoprobe + + trap_probe(probe, ruby_program) { |d_file, rb_file, probes| + foo_calls = probes.map { |line| line.split }.find_all { |row| + row[1] == 'times' + } + + assert_equal 1, foo_calls.length + } + end + + def test_exit + probe = <<-eoprobe +ruby$target:::cmethod-return +{ + printf("%s %s %s %d\\n", copyinstr(arg0), copyinstr(arg1), copyinstr(arg2), arg3); +} + eoprobe + + trap_probe(probe, ruby_program) { |d_file, rb_file, probes| + foo_calls = probes.map { |line| line.split }.find_all { |row| + row[1] == 'times' + } + + assert_equal 1, foo_calls.length + } + end + + def ruby_program + <<-eoruby + class Foo + def self.foo; end + end + 10.times { Foo.foo } + eoruby + end + end +end if defined?(DTrace::TestCase) + diff --git a/test/dtrace/test_function_entry.rb b/test/dtrace/test_function_entry.rb new file mode 100644 index 0000000000..e2395ab15a --- /dev/null +++ b/test/dtrace/test_function_entry.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: false +require_relative 'helper' + +module DTrace + class TestFunctionEntry < TestCase + def test_function_entry + probe = <<-eoprobe +ruby$target:::method-entry +/arg0 && arg1 && arg2/ +{ + printf("%s %s %s %d\\n", copyinstr(arg0), copyinstr(arg1), copyinstr(arg2), arg3); +} + eoprobe + + trap_probe(probe, ruby_program) { |d_file, rb_file, probes| + foo_calls = probes.map { |line| line.split }.find_all { |row| + row.first == 'Foo' && row[1] == 'foo' + } + + assert_equal 10, foo_calls.length, probes + line = '3' + foo_calls.each { |f| assert_equal line, f[3] } + foo_calls.each { |f| assert_equal rb_file, f[2] } + } + end + + def test_function_return + probe = <<-eoprobe +ruby$target:::method-return +/arg0 && arg1 && arg2/ +{ + printf("%s %s %s %d\\n", copyinstr(arg0), copyinstr(arg1), copyinstr(arg2), arg3); +} + eoprobe + + trap_probe(probe, ruby_program) { |d_file, rb_file, probes| + foo_calls = probes.map { |line| line.split }.find_all { |row| + row.first == 'Foo' && row[1] == 'foo' + } + + assert_equal 10, foo_calls.length, probes.inspect + line = '3' + foo_calls.each { |f| assert_equal line, f[3] } + foo_calls.each { |f| assert_equal rb_file, f[2] } + } + end + + def test_return_from_raise + program = <<-eoruby + class Foo + def bar; raise; end + def baz + bar + rescue + end + end + + Foo.new.baz + eoruby + + probe = <<-eoprobe +ruby$target:::method-return +/arg0 && arg1 && arg2/ +{ + printf("%s %s %s %d\\n", copyinstr(arg0), copyinstr(arg1), copyinstr(arg2), arg3); +} + eoprobe + + trap_probe(probe, program) { |d_file, rb_file, probes| + foo_calls = probes.map { |line| line.split }.find_all { |row| + row.first == 'Foo' && row[1] == 'bar' + } + assert foo_calls.any? + } + end + + private + def ruby_program + <<-eoruby + TracePoint.new{}.__enable(nil, nil, Thread.current) + class Foo + def foo; end + end + x = Foo.new + 10.times { x.foo } + eoruby + end + end +end if defined?(DTrace::TestCase) diff --git a/test/dtrace/test_gc.rb b/test/dtrace/test_gc.rb new file mode 100644 index 0000000000..77de7998dd --- /dev/null +++ b/test/dtrace/test_gc.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: false +require_relative 'helper' + +module DTrace + class TestGC < TestCase + %w{ + gc-mark-begin + gc-mark-end + gc-sweep-begin + gc-sweep-end + }.each do |probe_name| + define_method(:"test_#{probe_name.gsub(/-/, '_')}") do + probe = "ruby$target:::#{probe_name} { printf(\"#{probe_name}\\n\"); }" + + trap_probe(probe, ruby_program) { |_, _, saw| + assert_operator saw.length, :>, 0 + } + + end + end + + private + def ruby_program + "100000.times { Object.new }" + end + end +end if defined?(DTrace::TestCase) diff --git a/test/dtrace/test_hash_create.rb b/test/dtrace/test_hash_create.rb new file mode 100644 index 0000000000..603ee21872 --- /dev/null +++ b/test/dtrace/test_hash_create.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: false +require_relative 'helper' + +module DTrace + class TestHashCreate < TestCase + def test_hash_new + trap_probe(probe, 'Hash.new') { |_,rbfile,saw| + saw = saw.map(&:split).find_all { |num, file, line| + file == rbfile && num == '0' + } + assert_operator saw.length, :>, 0 + } + end + + def test_hash_lit + trap_probe(probe, '{}') { |_,rbfile,saw| + saw = saw.map(&:split).find_all { |num, file, line| + file == rbfile && num == '0' + } + assert_operator saw.length, :>, 0 + } + end + + def test_hash_lit_elements + trap_probe(probe, '{ :foo => :bar }') { |_,rbfile,orig| + saw = orig.map(&:split).find_all { |num, file, line| + file == rbfile && num == '2' + } + assert_operator saw.length, :>, 0, orig + } + end + + def test_hash_lit_elements_string + trap_probe(probe, '{ :foo => :bar, :bar => "baz" }') { |_,rbfile,saw| + saw = saw.map(&:split).find_all { |num, file, line| + file == rbfile && num == '4' + } + assert_operator saw.length, :>, 0 + } + end + + private + def probe + <<-eoprobe +ruby$target:::hash-create +/arg1/ +{ + printf("%d %s %d\\n", arg0, copyinstr(arg1), arg2); +} + eoprobe + end + end +end if defined?(DTrace::TestCase) diff --git a/test/dtrace/test_load.rb b/test/dtrace/test_load.rb new file mode 100644 index 0000000000..1c208bd488 --- /dev/null +++ b/test/dtrace/test_load.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: false +require_relative 'helper' +require 'tempfile' + +module DTrace + class TestLoad < TestCase + def setup + super + @rbfile = Tempfile.new(['omg', 'rb']) + @rbfile.write 'x = 10' + end + + def teardown + super + @rbfile.close(true) if @rbfile + end + + def test_load_entry + probe = <<-eoprobe +ruby$target:::load-entry +{ + printf("%s %s %d\\n", copyinstr(arg0), copyinstr(arg1), arg2); +} + eoprobe + trap_probe(probe, program) { |dpath, rbpath, saw| + saw = saw.map(&:split).find_all { |loaded, _, _| + loaded == @rbfile.path + } + assert_equal 10, saw.length + } + end + + def test_load_return + probe = <<-eoprobe +ruby$target:::load-return +{ + printf("%s\\n", copyinstr(arg0)); +} + eoprobe + trap_probe(probe, program) { |dpath, rbpath, saw| + saw = saw.map(&:split).find_all { |loaded, _, _| + loaded == @rbfile.path + } + assert_equal 10, saw.length + } + end + + private + def program + "10.times { load '#{@rbfile.path}' }" + end + end +end if defined?(DTrace::TestCase) diff --git a/test/dtrace/test_method_cache.rb b/test/dtrace/test_method_cache.rb new file mode 100644 index 0000000000..88b927464a --- /dev/null +++ b/test/dtrace/test_method_cache.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: false +require_relative 'helper' + +module DTrace + class TestMethodCacheClear < TestCase + def test_method_cache_clear + trap_probe(probe, <<-code) do |_,rbfile,lines| + class String; end + class String; def abc() end end + class Object; def abc() end end + code + assert_not_include lines, "String #{rbfile} 1\n" + assert_include lines, "String #{rbfile} 2\n" + assert_include lines, "global #{rbfile} 3\n" + end + end + + private + def probe + <<-eoprobe +ruby$target:::method-cache-clear +/arg1 && arg2/ +{ + printf("%s %s %d\\n", copyinstr(arg0), copyinstr(arg1), arg2); +} + eoprobe + end + end +end if defined?(DTrace::TestCase) diff --git a/test/dtrace/test_object_create_start.rb b/test/dtrace/test_object_create_start.rb new file mode 100644 index 0000000000..81f8ed2110 --- /dev/null +++ b/test/dtrace/test_object_create_start.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: false +require_relative 'helper' + +module DTrace + class TestObjectCreateStart < TestCase + def test_object_create_start + trap_probe(probe, '10.times { Object.new }') { |_,rbfile,saw| + saw = saw.map(&:split).find_all { |_, file, _| + file == rbfile + } + assert_equal 10, saw.length + } + end + + def test_object_create_start_name + trap_probe(probe, 'Hash.new') { |_,rbfile,saw| + saw = saw.map(&:split).find_all { |klass, file, line| + file == rbfile + } + assert_equal(%w{ Hash }, saw.map(&:first)) + assert_equal([rbfile], saw.map { |line| line[1] }) + assert_equal(['1'], saw.map { |line| line[2] }) + } + end + + private + def probe + <<-eoprobe +ruby$target:::object-create +{ + printf("%s %s %d\\n", copyinstr(arg0), copyinstr(arg1), arg2); +} + eoprobe + end + end +end if defined?(DTrace::TestCase) diff --git a/test/dtrace/test_raise.rb b/test/dtrace/test_raise.rb new file mode 100644 index 0000000000..81e64e8c16 --- /dev/null +++ b/test/dtrace/test_raise.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: false +require_relative 'helper' + +module DTrace + class TestRaise < TestCase + def test_raise + probe = <<-eoprobe +ruby$target:::raise +{ + printf("%s %s %d\\n", copyinstr(arg0), copyinstr(arg1), arg2); +} + eoprobe + trap_probe(probe, program) { |dpath, rbpath, saw| + saw = saw.map(&:split).find_all { |_, source_file, _| + source_file == rbpath + } + assert_equal 10, saw.length + saw.each do |klass, _, source_line| + assert_equal 'RuntimeError', klass + assert_equal '1', source_line + end + } + end + + private + def program + '10.times { raise rescue nil }' + end + end +end if defined?(DTrace::TestCase) diff --git a/test/dtrace/test_require.rb b/test/dtrace/test_require.rb new file mode 100644 index 0000000000..da5c08f7fc --- /dev/null +++ b/test/dtrace/test_require.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: false +require_relative 'helper' + +module DTrace + class TestRequire < TestCase + def test_require_entry + probe = <<-eoprobe +ruby$target:::require-entry +{ + printf("%s %s %d\\n", copyinstr(arg0), copyinstr(arg1), arg2); +} + eoprobe + trap_probe(probe, ruby_program) { |d_file, rb_file, saw| + required = saw.map { |s| s.split }.find_all do |(required, _)| + required == 'dtrace/dummy' + end + assert_equal 10, required.length + } + end + + def test_require_return + probe = <<-eoprobe +ruby$target:::require-return +{ + printf("%s\\n", copyinstr(arg0)); +} + eoprobe + trap_probe(probe, ruby_program) { |d_file, rb_file, saw| + required = saw.map { |s| s.split }.find_all do |(required, _)| + required == 'dtrace/dummy' + end + assert_equal 10, required.length + } + end + + private + def ruby_program + "10.times { require 'dtrace/dummy' }" + end + end +end if defined?(DTrace::TestCase) diff --git a/test/dtrace/test_singleton_function.rb b/test/dtrace/test_singleton_function.rb new file mode 100644 index 0000000000..bad1fa0692 --- /dev/null +++ b/test/dtrace/test_singleton_function.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: false +require_relative 'helper' + +module DTrace + class TestSingletonFunctionEntry < TestCase + def test_entry + probe = <<-eoprobe +ruby$target:::method-entry +/strstr(copyinstr(arg0), "Foo") != NULL/ +{ + printf("%s %s %s %d\\n", copyinstr(arg0), copyinstr(arg1), copyinstr(arg2), arg3); +} + eoprobe + + trap_probe(probe, ruby_program) { |d_file, rb_file, probes| + foo_calls = probes.map { |line| line.split }.find_all { |row| + row.first == 'Foo' && row[1] == 'foo' + } + + assert_equal 10, foo_calls.length, probes.inspect + line = '3' + foo_calls.each { |f| assert_equal line, f[3] } + foo_calls.each { |f| assert_equal rb_file, f[2] } + } + end + + def test_exit + probe = <<-eoprobe +ruby$target:::method-return +{ + printf("%s %s %s %d\\n", copyinstr(arg0), copyinstr(arg1), copyinstr(arg2), arg3); +} + eoprobe + + trap_probe(probe, ruby_program) { |d_file, rb_file, probes| + foo_calls = probes.map { |line| line.split }.find_all { |row| + row.first == 'Foo' && row[1] == 'foo' + } + + assert_equal 10, foo_calls.length, probes.inspect + line = '3' + foo_calls.each { |f| assert_equal line, f[3] } + foo_calls.each { |f| assert_equal rb_file, f[2] } + } + end + + def ruby_program + <<-eoruby + TracePoint.new{}.__enable(nil, nil, Thread.current) + class Foo + def self.foo; end + end + 10.times { Foo.foo } + eoruby + end + end +end if defined?(DTrace::TestCase) diff --git a/test/dtrace/test_string.rb b/test/dtrace/test_string.rb new file mode 100644 index 0000000000..a72f989f63 --- /dev/null +++ b/test/dtrace/test_string.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: false +require_relative 'helper' + +module DTrace + class TestStringProbes < TestCase + def test_object_create_start_string_lit + trap_probe(probe, '"omglolwutbbq"') { |_,rbfile,orig| + saw = orig.map(&:split).find_all { |klass, file, line, len| + file == rbfile && len == '12' && line == '1' + } + assert_equal(%w{ String }, saw.map(&:first), orig.inspect) + assert_equal([rbfile], saw.map { |line| line[1] }) + assert_equal(['1'], saw.map { |line| line[2] }) + } + end + + private + def probe + <<-eoprobe +ruby$target:::string-create +/arg1/ +{ + printf("String %s %d %d\\n", copyinstr(arg1), arg2, arg0); +} + eoprobe + end + end +end if defined?(DTrace::TestCase) |
