diff --git a/lib/argument_parser.rb b/lib/argument_parser.rb new file mode 100644 index 00000000..5b961af9 --- /dev/null +++ b/lib/argument_parser.rb @@ -0,0 +1,185 @@ +require 'optparse' +require 'shellwords' +require 'rbconfig' + +class ArgumentParser + Args = Struct.new( + :executables, + :out_path, + :out_override, + :harness, + :yjit_opts, + :categories, + :name_filters, + :rss, + :graph, + :no_pinning, + :turbo, + :skip_yjit, + :with_pre_init, + keyword_init: true + ) + + def self.parse(argv = ARGV, ruby_executable: RbConfig.ruby) + new(ruby_executable: ruby_executable).parse(argv) + end + + def initialize(ruby_executable: RbConfig.ruby) + @ruby_executable = ruby_executable + end + + def parse(argv) + args = default_args + + OptionParser.new do |opts| + opts.on("-e=NAME::RUBY_PATH OPTIONS", "ruby executable and options to be benchmarked (default: interp, yjit)") do |v| + v.split(";").each do |name_executable| + name, executable = name_executable.split("::", 2) + if executable.nil? + executable = name # allow skipping `NAME::` + end + args.executables[name] = executable.shellsplit + end + end + + opts.on("--chruby=NAME::VERSION OPTIONS", "ruby version under chruby and options to be benchmarked") do |v| + v.split(";").each do |name_version| + name, version = name_version.split("::", 2) + # Convert `ruby --yjit` to `ruby::ruby --yjit` + if version.nil? + version = name + name = name.shellsplit.first + end + version, *options = version.shellsplit + rubies_dir = ENV["RUBIES_DIR"] || "#{ENV["HOME"]}/.rubies" + unless executable = ["/opt/rubies/#{version}/bin/ruby", "#{rubies_dir}/#{version}/bin/ruby"].find { |path| File.executable?(path) } + abort "Cannot find '#{version}' in /opt/rubies or #{rubies_dir}" + end + args.executables[name] = [executable, *options] + end + end + + opts.on("--out_path=OUT_PATH", "directory where to store output data files") do |v| + args.out_path = v + end + + opts.on("--out-name=OUT_FILE", "write exactly this output file plus file extension, ignoring directories, overwriting if necessary") do |v| + args.out_override = v + end + + opts.on("--category=headline,other,micro,ractor", "when given, only benchmarks with specified categories will run") do |v| + args.categories += v.split(",") + if args.categories == ["ractor"] + args.harness = "harness-ractor" + end + end + + opts.on("--headline", "when given, headline benchmarks will be run") do + args.categories += ["headline"] + end + + opts.on("--name_filters=x,y,z", Array, "when given, only benchmarks with names that contain one of these strings will run") do |list| + args.name_filters = list + end + + opts.on("--skip-yjit", "Don't run with yjit after interpreter") do + args.skip_yjit = true + end + + opts.on("--harness=HARNESS_DIR", "which harness to use") do |v| + v = "harness-#{v}" unless v.start_with?('harness') + args.harness = v + end + + opts.on("--warmup=N", "the number of warmup iterations for the default harness (default: 15)") do |n| + ENV["WARMUP_ITRS"] = n + end + + opts.on("--bench=N", "the number of benchmark iterations for the default harness (default: 10). Also defaults MIN_BENCH_TIME to 0.") do |n| + ENV["MIN_BENCH_ITRS"] = n + ENV["MIN_BENCH_TIME"] ||= "0" + end + + opts.on("--once", "benchmarks only 1 iteration with no warmup for the default harness") do + ENV["WARMUP_ITRS"] = "0" + ENV["MIN_BENCH_ITRS"] = "1" + ENV["MIN_BENCH_TIME"] = "0" + end + + opts.on("--yjit-stats=STATS", "print YJIT stats at each iteration for the default harness") do |str| + ENV["YJIT_BENCH_STATS"] = str + end + + opts.on("--zjit-stats=STATS", "print ZJIT stats at each iteration for the default harness") do |str| + ENV["ZJIT_BENCH_STATS"] = str + end + + opts.on("--yjit_opts=OPT_STRING", "string of command-line options to run YJIT with (ignored if you use -e)") do |str| + args.yjit_opts = str + end + + opts.on("--with_pre-init=PRE_INIT_FILE", + "a file to require before each benchmark run, so settings can be tuned (eg. enable/disable GC compaction)") do |str| + args.with_pre_init = str + end + + opts.on("--rss", "show RSS in the output (measured after benchmark iterations)") do + args.rss = true + end + + opts.on("--graph", "generate a graph image of benchmark results") do + args.graph = true + end + + opts.on("--no-pinning", "don't pin ruby to a specific CPU core") do + args.no_pinning = true + end + + opts.on("--turbo", "don't disable CPU turbo boost") do + args.turbo = true + end + end.parse!(argv) + + # Remaining arguments are treated as benchmark name filters + if argv.length > 0 + args.name_filters += argv + end + + # If -e is not specified, benchmark the current Ruby. Compare it with YJIT if available. + if args.executables.empty? + if have_yjit?(@ruby_executable) && !args.skip_yjit + args.executables["interp"] = [@ruby_executable] + args.executables["yjit"] = [@ruby_executable, "--yjit", *args.yjit_opts.shellsplit] + else + args.executables["ruby"] = [@ruby_executable] + end + end + + args + end + + private + + def have_yjit?(ruby) + ruby_version = `#{ruby} -v --yjit 2> #{File::NULL}`.strip + ruby_version.downcase.include?("yjit") + end + + def default_args + Args.new( + executables: {}, + out_path: File.expand_path("./data"), + out_override: nil, + harness: "harness", + yjit_opts: "", + categories: [], + name_filters: [], + rss: false, + graph: false, + no_pinning: false, + turbo: false, + skip_yjit: false, + with_pre_init: nil, + ) + end +end diff --git a/run_benchmarks.rb b/run_benchmarks.rb index 55d32693..9c7038d5 100755 --- a/run_benchmarks.rb +++ b/run_benchmarks.rb @@ -1,12 +1,10 @@ #!/usr/bin/env ruby -require 'optparse' -require 'ostruct' require 'pathname' require 'fileutils' -require 'shellwords' require 'csv' require 'json' +require 'shellwords' require 'rbconfig' require 'etc' require 'yaml' @@ -15,11 +13,7 @@ require_relative 'lib/benchmark_runner' require_relative 'lib/table_formatter' require_relative 'lib/benchmark_filter' - -def have_yjit?(ruby) - ruby_version = `#{ruby} -v --yjit 2> #{File::NULL}`.strip - ruby_version.downcase.include?("yjit") -end +require_relative 'lib/argument_parser' def mean(values) Stats.new(values).mean @@ -158,145 +152,7 @@ def run_benchmarks(ruby:, ruby_description:, categories:, name_filters:, out_pat [bench_data, bench_failures] end -# Default values for command-line arguments -args = OpenStruct.new({ - executables: {}, - out_path: File.expand_path("./data"), - out_override: nil, - harness: "harness", - yjit_opts: "", - categories: [], - name_filters: [], - rss: false, - graph: false, - no_pinning: false, - turbo: false, - skip_yjit: false, -}) - -OptionParser.new do |opts| - opts.on("-e=NAME::RUBY_PATH OPTIONS", "ruby executable and options to be benchmarked (default: interp, yjit)") do |v| - v.split(";").each do |name_executable| - name, executable = name_executable.split("::", 2) - if executable.nil? - executable = name # allow skipping `NAME::` - end - args.executables[name] = executable.shellsplit - end - end - - opts.on("--chruby=NAME::VERSION OPTIONS", "ruby version under chruby and options to be benchmarked") do |v| - v.split(";").each do |name_version| - name, version = name_version.split("::", 2) - # Convert `ruby --yjit` to `ruby::ruby --yjit` - if version.nil? - version = name - name = name.shellsplit.first - end - version, *options = version.shellsplit - rubies_dir = ENV["RUBIES_DIR"] || "#{ENV["HOME"]}/.rubies" - unless executable = ["/opt/rubies/#{version}/bin/ruby", "#{rubies_dir}/#{version}/bin/ruby"].find { |path| File.executable?(path) } - abort "Cannot find '#{version}' in /opt/rubies or #{rubies_dir}" - end - args.executables[name] = [executable, *options] - end - end - - opts.on("--out_path=OUT_PATH", "directory where to store output data files") do |v| - args.out_path = v - end - - opts.on("--out-name=OUT_FILE", "write exactly this output file plus file extension, ignoring directories, overwriting if necessary") do |v| - args.out_override = v - end - - opts.on("--category=headline,other,micro,ractor", "when given, only benchmarks with specified categories will run") do |v| - args.categories += v.split(",") - if args.categories == ["ractor"] - args.harness = "harness-ractor" - end - end - - opts.on("--headline", "when given, headline benchmarks will be run") do - args.categories += ["headline"] - end - - opts.on("--name_filters=x,y,z", Array, "when given, only benchmarks with names that contain one of these strings will run") do |list| - args.name_filters = list - end - - opts.on("--skip-yjit", "Don't run with yjit after interpreter") do - args.skip_yjit = true - end - - opts.on("--harness=HARNESS_DIR", "which harness to use") do |v| - v = "harness-#{v}" unless v.start_with?('harness') - args.harness = v - end - - opts.on("--warmup=N", "the number of warmup iterations for the default harness (default: 15)") do |n| - ENV["WARMUP_ITRS"] = n - end - - opts.on("--bench=N", "the number of benchmark iterations for the default harness (default: 10). Also defaults MIN_BENCH_TIME to 0.") do |n| - ENV["MIN_BENCH_ITRS"] = n - ENV["MIN_BENCH_TIME"] ||= "0" - end - - opts.on("--once", "benchmarks only 1 iteration with no warmup for the default harness") do - ENV["WARMUP_ITRS"] = "0" - ENV["MIN_BENCH_ITRS"] = "1" - ENV["MIN_BENCH_TIME"] = "0" - end - - opts.on("--yjit-stats=STATS", "print YJIT stats at each iteration for the default harness") do |str| - ENV["YJIT_BENCH_STATS"] = str - end - - opts.on("--zjit-stats=STATS", "print ZJIT stats at each iteration for the default harness") do |str| - ENV["ZJIT_BENCH_STATS"] = str - end - - opts.on("--yjit_opts=OPT_STRING", "string of command-line options to run YJIT with (ignored if you use -e)") do |str| - args.yjit_opts=str - end - - opts.on("--with_pre-init=PRE_INIT_FILE", - "a file to require before each benchmark run, so settings can be tuned (eg. enable/disable GC compaction)") do |str| - args.with_pre_init = str - end - - opts.on("--rss", "show RSS in the output (measured after benchmark iterations)") do - args.rss = true - end - - opts.on("--graph", "generate a graph image of benchmark results") do - args.graph = true - end - - opts.on("--no-pinning", "don't pin ruby to a specific CPU core") do - args.no_pinning = true - end - - opts.on("--turbo", "don't disable CPU turbo boost") do - args.turbo = true - end -end.parse! - -# Remaining arguments are treated as benchmark name filters -if ARGV.length > 0 - args.name_filters += ARGV -end - -# If -e is not specified, benchmark the current Ruby. Compare it with YJIT if available. -if args.executables.empty? - if have_yjit?(RbConfig.ruby) && !args.skip_yjit - args.executables["interp"] = [RbConfig.ruby] - args.executables["yjit"] = [RbConfig.ruby, "--yjit", *args.yjit_opts.shellsplit] - else - args.executables["ruby"] = [RbConfig.ruby] - end -end +args = ArgumentParser.parse(ARGV) CPUConfig.configure_for_benchmarking(turbo: args.turbo) diff --git a/test/argument_parser_test.rb b/test/argument_parser_test.rb new file mode 100644 index 00000000..7008dcef --- /dev/null +++ b/test/argument_parser_test.rb @@ -0,0 +1,601 @@ +require_relative 'test_helper' +require_relative '../lib/argument_parser' +require 'tmpdir' +require 'fileutils' + +describe ArgumentParser do + before do + @original_env = {} + ['WARMUP_ITRS', 'MIN_BENCH_ITRS', 'MIN_BENCH_TIME', 'YJIT_BENCH_STATS', + 'ZJIT_BENCH_STATS', 'RUBIES_DIR', 'HOME'].each do |key| + @original_env[key] = ENV[key] + end + end + + after do + @original_env.each do |key, value| + if value.nil? + ENV.delete(key) + else + ENV[key] = value + end + end + + if @temp_home && Dir.exist?(@temp_home) + FileUtils.rm_rf(@temp_home) + end + end + + def setup_mock_ruby(path) + FileUtils.mkdir_p(File.dirname(path)) + File.write(path, "#!/bin/sh\necho 'mock ruby'\n") + File.chmod(0755, path) + end + + describe '#parse' do + it 'returns default values when no arguments provided' do + mock_ruby = '/usr/bin/ruby' + parser = ArgumentParser.new(ruby_executable: mock_ruby) + + # Stub to return false so we get a single 'ruby' executable + parser.stub :have_yjit?, false do + args = parser.parse([]) + + assert_equal({ 'ruby' => [mock_ruby] }, args.executables) + assert_equal File.expand_path("./data"), args.out_path + assert_nil args.out_override + assert_equal "harness", args.harness + assert_equal "", args.yjit_opts + assert_equal [], args.categories + assert_equal [], args.name_filters + assert_equal false, args.rss + assert_equal false, args.graph + assert_equal false, args.no_pinning + assert_equal false, args.turbo + assert_equal false, args.skip_yjit + end + end + + describe '-e option' do + it 'parses single executable' do + parser = ArgumentParser.new + args = parser.parse(['-e', 'test::ruby']) + + assert_equal({ 'test' => ['ruby'] }, args.executables) + end + + it 'parses executable with options' do + parser = ArgumentParser.new + args = parser.parse(['-e', 'yjit::ruby --yjit']) + + assert_equal({ 'yjit' => ['ruby', '--yjit'] }, args.executables) + end + + it 'parses multiple executables with semicolon' do + parser = ArgumentParser.new + args = parser.parse(['-e', 'interp::ruby;yjit::ruby --yjit']) + + expected = { + 'interp' => ['ruby'], + 'yjit' => ['ruby', '--yjit'] + } + assert_equal expected, args.executables + end + + it 'allows skipping NAME:: prefix' do + parser = ArgumentParser.new + args = parser.parse(['-e', 'ruby']) + + assert_equal({ 'ruby' => ['ruby'] }, args.executables) + end + + it 'parses complex options with quotes' do + parser = ArgumentParser.new + args = parser.parse(['-e', 'test::ruby --yjit-call-threshold=10']) + + assert_equal({ 'test' => ['ruby', '--yjit-call-threshold=10'] }, args.executables) + end + end + + describe '--chruby option' do + it 'finds ruby in /opt/rubies' do + Dir.mktmpdir do |tmpdir| + ruby_path = File.join(tmpdir, 'opt/rubies/3.2.0/bin/ruby') + setup_mock_ruby(ruby_path) + + File.stub :executable?, ->(path) { + if path == "/opt/rubies/3.2.0/bin/ruby" + File.exist?(ruby_path) && File.stat(ruby_path).executable? + end + } do + parser = ArgumentParser.new + args = parser.parse(['--chruby=ruby-3.2.0::3.2.0']) + + assert_equal '/opt/rubies/3.2.0/bin/ruby', args.executables['ruby-3.2.0'].first + end + end + end + + it 'finds ruby in RUBIES_DIR' do + Dir.mktmpdir do |tmpdir| + @temp_home = tmpdir + rubies_dir = File.join(tmpdir, '.rubies') + ruby_path = File.join(rubies_dir, '3.3.0/bin/ruby') + setup_mock_ruby(ruby_path) + + ENV['HOME'] = tmpdir + + parser = ArgumentParser.new + args = parser.parse(['--chruby=my-ruby::3.3.0']) + + assert_equal ruby_path, args.executables['my-ruby'].first + end + end + + it 'prefers /opt/rubies over RUBIES_DIR' do + Dir.mktmpdir do |tmpdir| + @temp_home = tmpdir + + opt_ruby = File.join(tmpdir, 'opt/rubies/3.2.0/bin/ruby') + home_ruby = File.join(tmpdir, '.rubies/3.2.0/bin/ruby') + setup_mock_ruby(opt_ruby) + setup_mock_ruby(home_ruby) + + ENV['HOME'] = tmpdir + + File.stub :executable?, ->(path) { + case path + when "/opt/rubies/3.2.0/bin/ruby" + File.exist?(opt_ruby) && File.stat(opt_ruby).executable? + when "#{tmpdir}/.rubies/3.2.0/bin/ruby" + File.exist?(home_ruby) && File.stat(home_ruby).executable? + else + File.method(:executable?).super_method.call(path) + end + } do + parser = ArgumentParser.new + args = parser.parse(['--chruby=test::3.2.0']) + + assert_equal '/opt/rubies/3.2.0/bin/ruby', args.executables['test'].first + end + end + end + + it 'uses RUBIES_DIR environment variable when set' do + Dir.mktmpdir do |tmpdir| + @temp_home = tmpdir + custom_rubies = File.join(tmpdir, 'custom_rubies') + ruby_path = File.join(custom_rubies, '3.4.0/bin/ruby') + setup_mock_ruby(ruby_path) + + ENV['RUBIES_DIR'] = custom_rubies + + parser = ArgumentParser.new + args = parser.parse(['--chruby=custom::3.4.0']) + + assert_equal ruby_path, args.executables['custom'].first + end + end + + it 'aborts when ruby version not found' do + Dir.mktmpdir do |tmpdir| + @temp_home = tmpdir + ENV['HOME'] = tmpdir + + parser = ArgumentParser.new + + assert_raises(SystemExit) do + capture_io do + parser.parse(['--chruby=nonexistent::nonexistent-version-999']) + end + end + end + end + + it 'parses version with options' do + Dir.mktmpdir do |tmpdir| + @temp_home = tmpdir + rubies_dir = File.join(tmpdir, '.rubies') + ruby_path = File.join(rubies_dir, '3.2.0/bin/ruby') + setup_mock_ruby(ruby_path) + + ENV['HOME'] = tmpdir + + parser = ArgumentParser.new + args = parser.parse(['--chruby=yjit::3.2.0 --yjit']) + + assert_equal ruby_path, args.executables['yjit'].first + assert_equal '--yjit', args.executables['yjit'].last + end + end + + it 'allows skipping NAME:: prefix and uses first word as name' do + Dir.mktmpdir do |tmpdir| + @temp_home = tmpdir + rubies_dir = File.join(tmpdir, '.rubies') + ruby_path = File.join(rubies_dir, '3.2.0/bin/ruby') + setup_mock_ruby(ruby_path) + + ENV['HOME'] = tmpdir + + parser = ArgumentParser.new + args = parser.parse(['--chruby=3.2.0 --yjit']) + + assert args.executables.key?('3.2.0') + assert_equal ruby_path, args.executables['3.2.0'].first + end + end + + it 'handles semicolon-separated multiple versions' do + Dir.mktmpdir do |tmpdir| + @temp_home = tmpdir + rubies_dir = File.join(tmpdir, '.rubies') + ruby_path_32 = File.join(rubies_dir, '3.2.0/bin/ruby') + ruby_path_33 = File.join(rubies_dir, '3.3.0/bin/ruby') + setup_mock_ruby(ruby_path_32) + setup_mock_ruby(ruby_path_33) + + ENV['HOME'] = tmpdir + + parser = ArgumentParser.new + args = parser.parse(['--chruby=ruby32::3.2.0;ruby33::3.3.0 --yjit']) + + assert_equal 2, args.executables.size + assert_equal ruby_path_32, args.executables['ruby32'].first + assert_equal ruby_path_33, args.executables['ruby33'].first + assert_equal '--yjit', args.executables['ruby33'].last + end + end + end + + describe '--out_path option' do + it 'sets output path' do + parser = ArgumentParser.new + args = parser.parse(['--out_path=/tmp/results']) + + assert_equal '/tmp/results', args.out_path + end + end + + describe '--out-name option' do + it 'sets output override name' do + parser = ArgumentParser.new + args = parser.parse(['--out-name=my_results']) + + assert_equal 'my_results', args.out_override + end + end + + describe '--category option' do + it 'parses single category' do + parser = ArgumentParser.new + args = parser.parse(['--category=headline']) + + assert_equal ['headline'], args.categories + end + + it 'parses multiple categories' do + parser = ArgumentParser.new + args = parser.parse(['--category=headline,micro']) + + assert_equal ['headline', 'micro'], args.categories + end + + it 'sets harness to harness-ractor when category is ractor' do + parser = ArgumentParser.new + args = parser.parse(['--category=ractor']) + + assert_equal ['ractor'], args.categories + assert_equal 'harness-ractor', args.harness + end + + it 'allows multiple category flags' do + parser = ArgumentParser.new + args = parser.parse(['--category=headline', '--category=micro']) + + assert_equal ['headline', 'micro'], args.categories + end + end + + describe '--headline option' do + it 'adds headline to categories' do + parser = ArgumentParser.new + args = parser.parse(['--headline']) + + assert_equal ['headline'], args.categories + end + + it 'can be combined with other categories' do + parser = ArgumentParser.new + args = parser.parse(['--headline', '--category=micro']) + + assert_equal ['headline', 'micro'], args.categories + end + end + + describe '--name_filters option' do + it 'parses single filter' do + parser = ArgumentParser.new + args = parser.parse(['--name_filters=fib']) + + assert_equal ['fib'], args.name_filters + end + + it 'parses multiple filters' do + parser = ArgumentParser.new + args = parser.parse(['--name_filters=fib,railsbench,optcarrot']) + + assert_equal ['fib', 'railsbench', 'optcarrot'], args.name_filters + end + end + + describe '--skip-yjit option' do + it 'sets skip_yjit flag' do + parser = ArgumentParser.new + args = parser.parse(['--skip-yjit']) + + assert_equal true, args.skip_yjit + end + end + + describe '--harness option' do + it 'sets harness directory' do + parser = ArgumentParser.new + args = parser.parse(['--harness=once']) + + assert_equal 'harness-once', args.harness + end + + it 'accepts harness- prefix' do + parser = ArgumentParser.new + args = parser.parse(['--harness=harness-stats']) + + assert_equal 'harness-stats', args.harness + end + end + + describe '--warmup option' do + it 'sets WARMUP_ITRS environment variable' do + parser = ArgumentParser.new + parser.parse(['--warmup=20']) + + assert_equal '20', ENV['WARMUP_ITRS'] + end + end + + describe '--bench option' do + it 'sets MIN_BENCH_ITRS and MIN_BENCH_TIME environment variables' do + parser = ArgumentParser.new + parser.parse(['--bench=5']) + + assert_equal '5', ENV['MIN_BENCH_ITRS'] + assert_equal '0', ENV['MIN_BENCH_TIME'] + end + end + + describe '--once option' do + it 'sets environment variables for single iteration' do + parser = ArgumentParser.new + parser.parse(['--once']) + + assert_equal '0', ENV['WARMUP_ITRS'] + assert_equal '1', ENV['MIN_BENCH_ITRS'] + assert_equal '0', ENV['MIN_BENCH_TIME'] + end + end + + describe '--yjit-stats option' do + it 'sets YJIT_BENCH_STATS environment variable' do + parser = ArgumentParser.new + parser.parse(['--yjit-stats=all']) + + assert_equal 'all', ENV['YJIT_BENCH_STATS'] + end + end + + describe '--zjit-stats option' do + it 'sets ZJIT_BENCH_STATS environment variable' do + parser = ArgumentParser.new + parser.parse(['--zjit-stats=all']) + + assert_equal 'all', ENV['ZJIT_BENCH_STATS'] + end + end + + describe '--yjit_opts option' do + it 'sets yjit_opts' do + parser = ArgumentParser.new + args = parser.parse(['--yjit_opts=--yjit-call-threshold=10']) + + assert_equal '--yjit-call-threshold=10', args.yjit_opts + end + end + + describe '--with_pre-init option' do + it 'sets with_pre_init' do + parser = ArgumentParser.new + args = parser.parse(['--with_pre-init=/path/to/init.rb']) + + assert_equal '/path/to/init.rb', args.with_pre_init + end + end + + describe '--rss option' do + it 'sets rss flag' do + parser = ArgumentParser.new + args = parser.parse(['--rss']) + + assert_equal true, args.rss + end + end + + describe '--graph option' do + it 'sets graph flag' do + parser = ArgumentParser.new + args = parser.parse(['--graph']) + + assert_equal true, args.graph + end + end + + describe '--no-pinning option' do + it 'sets no_pinning flag' do + parser = ArgumentParser.new + args = parser.parse(['--no-pinning']) + + assert_equal true, args.no_pinning + end + end + + describe '--turbo option' do + it 'sets turbo flag' do + parser = ArgumentParser.new + args = parser.parse(['--turbo']) + + assert_equal true, args.turbo + end + end + + describe 'remaining arguments' do + it 'treats remaining arguments as name filters' do + parser = ArgumentParser.new + args = parser.parse(['fib', 'railsbench']) + + assert_equal ['fib', 'railsbench'], args.name_filters + end + + it 'combines with --name_filters option' do + parser = ArgumentParser.new + args = parser.parse(['--name_filters=optcarrot', 'fib', 'railsbench']) + + assert_equal ['optcarrot', 'fib', 'railsbench'], args.name_filters + end + end + + describe 'combined options' do + it 'parses complex combination of options' do + parser = ArgumentParser.new + args = parser.parse([ + '-e=interp::ruby;yjit::ruby --yjit', + '--category=headline', + '--name_filters=rails', + '--out_path=/tmp', + '--rss', + '--graph', + '--no-pinning', + '--warmup=5', + '--bench=3', + 'optcarrot' + ]) + + assert_equal 2, args.executables.size + assert_equal ['headline'], args.categories + assert_equal ['rails', 'optcarrot'], args.name_filters + assert_equal '/tmp', args.out_path + assert_equal true, args.rss + assert_equal true, args.graph + assert_equal true, args.no_pinning + assert_equal '5', ENV['WARMUP_ITRS'] + assert_equal '3', ENV['MIN_BENCH_ITRS'] + end + end + + describe '.parse class method' do + it 'provides convenient shorthand' do + args = ArgumentParser.parse(['--rss']) + + assert_equal true, args.rss + end + end + + describe 'default executables' do + it 'sets ruby executable when no -e option and no YJIT' do + mock_ruby = '/usr/bin/ruby' + + parser = ArgumentParser.new(ruby_executable: mock_ruby) + + parser.stub :have_yjit?, false do + args = parser.parse([]) + + assert_equal 1, args.executables.size + assert_equal [mock_ruby], args.executables['ruby'] + end + end + + it 'sets interp and yjit executables when no -e option and YJIT available' do + mock_ruby = '/usr/bin/ruby' + + parser = ArgumentParser.new(ruby_executable: mock_ruby) + + parser.stub :have_yjit?, true do + args = parser.parse([]) + + assert_equal 2, args.executables.size + assert_equal [mock_ruby], args.executables['interp'] + assert_equal [mock_ruby, '--yjit'], args.executables['yjit'] + end + end + + it 'includes yjit_opts in default yjit executable' do + mock_ruby = '/usr/bin/ruby' + + parser = ArgumentParser.new(ruby_executable: mock_ruby) + + parser.stub :have_yjit?, true do + args = parser.parse(['--yjit_opts=--yjit-call-threshold=10']) + + assert_equal 2, args.executables.size + assert_equal [mock_ruby], args.executables['interp'] + assert_equal [mock_ruby, '--yjit', '--yjit-call-threshold=10'], args.executables['yjit'] + end + end + + it 'respects --skip-yjit flag when YJIT is available' do + mock_ruby = '/usr/bin/ruby' + + parser = ArgumentParser.new(ruby_executable: mock_ruby) + + parser.stub :have_yjit?, true do + args = parser.parse(['--skip-yjit']) + + assert_equal 1, args.executables.size + assert_equal [mock_ruby], args.executables['ruby'] + end + end + + it 'does not set default executables when -e option is provided' do + mock_ruby = '/usr/bin/ruby' + + parser = ArgumentParser.new(ruby_executable: mock_ruby) + + parser.stub :have_yjit?, true do + args = parser.parse(['-e', 'custom::custom-ruby']) + + assert_equal 1, args.executables.size + assert_equal ['custom-ruby'], args.executables['custom'] + end + end + + it 'does not set default executables when --chruby option is provided' do + Dir.mktmpdir do |tmpdir| + @temp_home = tmpdir + rubies_dir = File.join(tmpdir, '.rubies') + ruby_path = File.join(rubies_dir, '3.2.0/bin/ruby') + setup_mock_ruby(ruby_path) + + ENV['HOME'] = tmpdir + mock_ruby = '/usr/bin/ruby' + + parser = ArgumentParser.new(ruby_executable: mock_ruby) + + parser.stub :have_yjit?, true do + args = parser.parse(['--chruby=test::3.2.0']) + + assert_equal 1, args.executables.size + assert_equal ruby_path, args.executables['test'].first + end + end + end + end + end +end diff --git a/test/run_benchmarks_integration_test.rb b/test/run_benchmarks_integration_test.rb index 5142e069..2c21cb0a 100644 --- a/test/run_benchmarks_integration_test.rb +++ b/test/run_benchmarks_integration_test.rb @@ -96,19 +96,6 @@ end end - describe 'environment handling' do - it 'handles WARMUP_ITRS environment variable' do - # The script should read ENV["WARMUP_ITRS"] - script_content = File.read(@script_path) - assert_includes script_content, 'WARMUP_ITRS' - end - - it 'handles MIN_BENCH_ITRS environment variable' do - script_content = File.read(@script_path) - assert_includes script_content, 'MIN_BENCH_ITRS' - end - end - describe 'stats module integration' do it 'uses Stats class for calculations' do require_relative '../misc/stats'