Skip to content

Commit becf89f

Browse files
authored
Merge pull request #432 from ruby/rmf-add-tests
Add tests to the `run_benchmark` script and refactoring table formatting
2 parents c34aeca + 76750e8 commit becf89f

File tree

8 files changed

+851
-133
lines changed

8 files changed

+851
-133
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
ruby-version: ${{ matrix.ruby }}
2828

2929
- name: Run tests
30-
run: ruby test/benchmarks_test.rb
30+
run: rake test
3131

3232
- name: Test run_benchmarks.rb
3333
run: ./run_benchmarks.rb

Rakefile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
require 'rake/testtask'
4+
5+
desc 'Run all tests'
6+
Rake::TestTask.new(:test) do |t|
7+
t.libs << 'test'
8+
t.libs << 'lib'
9+
t.test_files = FileList['test/**/*_test.rb']
10+
t.verbose = true
11+
t.warning = true
12+
end
13+
14+
task default: :test

lib/benchmark_runner.rb

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# frozen_string_literal: true
2+
3+
require 'csv'
4+
require 'json'
5+
require 'rbconfig'
6+
7+
# Extracted helper methods from run_benchmarks.rb for testing
8+
module BenchmarkRunner
9+
module_function
10+
11+
# Find the first available file number for output files
12+
def free_file_no(directory)
13+
(1..).each do |file_no|
14+
out_path = File.join(directory, "output_%03d.csv" % file_no)
15+
return file_no unless File.exist?(out_path)
16+
end
17+
end
18+
19+
# Get benchmark categories from metadata
20+
def benchmark_categories(name, metadata)
21+
benchmark_metadata = metadata[name] || {}
22+
categories = [benchmark_metadata.fetch('category', 'other')]
23+
categories << 'ractor' if benchmark_metadata['ractor']
24+
categories
25+
end
26+
27+
# Check if the name matches any of the names in a list of filters
28+
def match_filter(entry, categories:, name_filters:, metadata:)
29+
name_filters = process_name_filters(name_filters)
30+
name = entry.sub(/\.rb\z/, '')
31+
(categories.empty? || benchmark_categories(name, metadata).any? { |cat| categories.include?(cat) }) &&
32+
(name_filters.empty? || name_filters.any? { |filter| filter === name })
33+
end
34+
35+
# Process "/my_benchmark/i" into /my_benchmark/i
36+
def process_name_filters(name_filters)
37+
name_filters.map do |name_filter|
38+
if name_filter[0] == "/"
39+
regexp_str = name_filter[1..-1].reverse.sub(/\A(\w*)\//, "")
40+
regexp_opts = ::Regexp.last_match(1).to_s
41+
regexp_str.reverse!
42+
r = /#{regexp_str}/
43+
if !regexp_opts.empty?
44+
# Convert option string to Regexp option flags
45+
flags = 0
46+
flags |= Regexp::IGNORECASE if regexp_opts.include?('i')
47+
flags |= Regexp::MULTILINE if regexp_opts.include?('m')
48+
flags |= Regexp::EXTENDED if regexp_opts.include?('x')
49+
r = Regexp.new(regexp_str, flags)
50+
end
51+
r
52+
else
53+
name_filter
54+
end
55+
end
56+
end
57+
58+
# Resolve the pre_init file path into a form that can be required
59+
def expand_pre_init(path)
60+
require 'pathname'
61+
62+
path = Pathname.new(path)
63+
64+
unless path.exist?
65+
puts "--with-pre-init called with non-existent file!"
66+
exit(-1)
67+
end
68+
69+
if path.directory?
70+
puts "--with-pre-init called with a directory, please pass a .rb file"
71+
exit(-1)
72+
end
73+
74+
library_name = path.basename(path.extname)
75+
load_path = path.parent.expand_path
76+
77+
[
78+
"-I", load_path,
79+
"-r", library_name
80+
]
81+
end
82+
83+
# Sort benchmarks with headlines first, then others, then micro
84+
def sort_benchmarks(bench_names, metadata)
85+
headline_benchmarks = metadata.select { |_, meta| meta['category'] == 'headline' }.keys
86+
micro_benchmarks = metadata.select { |_, meta| meta['category'] == 'micro' }.keys
87+
88+
headline_names, bench_names = bench_names.partition { |name| headline_benchmarks.include?(name) }
89+
micro_names, other_names = bench_names.partition { |name| micro_benchmarks.include?(name) }
90+
headline_names.sort + other_names.sort + micro_names.sort
91+
end
92+
93+
# Check which OS we are running
94+
def os
95+
@os ||= (
96+
host_os = RbConfig::CONFIG['host_os']
97+
case host_os
98+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
99+
:windows
100+
when /darwin|mac os/
101+
:macosx
102+
when /linux/
103+
:linux
104+
when /solaris|bsd/
105+
:unix
106+
else
107+
raise "unknown os: #{host_os.inspect}"
108+
end
109+
)
110+
end
111+
112+
# Generate setarch prefix for Linux
113+
def setarch_prefix
114+
# Disable address space randomization (for determinism)
115+
prefix = ["setarch", `uname -m`.strip, "-R"]
116+
117+
# Abort if we don't have permission (perhaps in a docker container).
118+
return [] unless system(*prefix, "true", out: File::NULL, err: File::NULL)
119+
120+
prefix
121+
end
122+
end

lib/table_formatter.rb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# frozen_string_literal: true
2+
3+
# Formats benchmark data as an ASCII table with aligned columns
4+
class TableFormatter
5+
COLUMN_SEPARATOR = ' '
6+
FAILURE_PLACEHOLDER = 'N/A'
7+
8+
def initialize(table_data, format, failures)
9+
@header = table_data.first
10+
@data_rows = table_data.drop(1)
11+
@format = format
12+
@failures = failures
13+
@num_columns = @header.size
14+
end
15+
16+
def to_s
17+
rows = build_all_rows
18+
col_widths = calculate_column_widths(rows)
19+
20+
format_table(rows, col_widths)
21+
end
22+
23+
private
24+
25+
attr_reader :num_columns
26+
27+
def build_all_rows
28+
[@header, *build_failure_rows, *build_formatted_data_rows]
29+
end
30+
31+
def build_failure_rows
32+
return [] if @failures.empty?
33+
34+
failed_benchmarks = extract_failed_benchmarks
35+
failed_benchmarks.map { |name| build_failure_row(name) }
36+
end
37+
38+
def extract_failed_benchmarks
39+
@failures.flat_map { |_exe, data| data.keys }.uniq
40+
end
41+
42+
def build_failure_row(benchmark_name)
43+
[benchmark_name, *Array.new(num_columns - 1, FAILURE_PLACEHOLDER)]
44+
end
45+
46+
def build_formatted_data_rows
47+
@data_rows.map { |row| apply_format(row) }
48+
end
49+
50+
def apply_format(row)
51+
@format.zip(row).map { |fmt, data| fmt % data }
52+
end
53+
54+
def calculate_column_widths(rows)
55+
(0...num_columns).map do |col_index|
56+
rows.map { |row| row[col_index].length }.max
57+
end
58+
end
59+
60+
def format_table(rows, col_widths)
61+
separator = build_separator(col_widths)
62+
63+
formatted_rows = rows.map { |row| format_row(row, col_widths) }
64+
65+
[separator, *formatted_rows, separator].join("\n") + "\n"
66+
end
67+
68+
def build_separator(col_widths)
69+
col_widths.map { |width| '-' * width }.join(COLUMN_SEPARATOR)
70+
end
71+
72+
def format_row(row, col_widths)
73+
row.map.with_index { |cell, i| cell.ljust(col_widths[i]) }
74+
.join(COLUMN_SEPARATOR)
75+
.rstrip
76+
end
77+
end

0 commit comments

Comments
 (0)