Skip to content

Commit 9b3a008

Browse files
committed
Extract table formatter and add tests
1 parent 5e14a1d commit 9b3a008

5 files changed

Lines changed: 198 additions & 97 deletions

File tree

lib/benchmark_runner.rb

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,6 @@
88
module BenchmarkRunner
99
module_function
1010

11-
# Format benchmark data as a string table
12-
def table_to_str(table_data, format, failures)
13-
# Trim numbers to one decimal for console display
14-
# Keep two decimals for the speedup ratios
15-
16-
failure_rows = failures.map { |_exe, data| data.keys }.flatten.uniq
17-
.map { |name| [name] + (['N/A'] * (table_data.first.size - 1)) }
18-
19-
table_data = table_data.first(1) + failure_rows + table_data.drop(1).map { |row|
20-
format.zip(row).map { |fmt, data| fmt % data }
21-
}
22-
23-
num_rows = table_data.length
24-
num_cols = table_data[0].length
25-
26-
# Pad each column to the maximum width in the column
27-
(0...num_cols).each do |c|
28-
cell_lens = (0...num_rows).map { |r| table_data[r][c].length }
29-
max_width = cell_lens.max
30-
(0...num_rows).each { |r| table_data[r][c] = table_data[r][c].ljust(max_width) }
31-
end
32-
33-
# Row of separator dashes
34-
sep_row = (0...num_cols).map { |i| '-' * table_data[0][i].length }.join(' ').rstrip
35-
36-
out = sep_row + "\n"
37-
38-
table_data.each do |row|
39-
out += row.join(' ').rstrip + "\n"
40-
end
41-
42-
out += sep_row + "\n"
43-
44-
out
45-
end
46-
4711
# Find the first available file number for output files
4812
def free_file_no(prefix)
4913
(1..).each do |file_no|

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

run_benchmarks.rb

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
require 'yaml'
1313
require_relative 'misc/stats'
1414
require_relative 'lib/benchmark_runner'
15+
require_relative 'lib/table_formatter'
1516

1617
# Check which OS we are running
1718
def os
@@ -109,10 +110,6 @@ def performance_governor?
109110
end
110111
end
111112

112-
def table_to_str(table_data, format, failures)
113-
BenchmarkRunner.table_to_str(table_data, format, failures)
114-
end
115-
116113
def mean(values)
117114
Stats.new(values).mean
118115
end
@@ -525,7 +522,7 @@ def run_benchmarks(ruby:, ruby_description:, categories:, name_filters:, out_pat
525522
output_str << "#{key}: #{value}\n"
526523
end
527524
output_str += "\n"
528-
output_str += table_to_str(table, format, bench_failures) + "\n"
525+
output_str += TableFormatter.new(table, format, bench_failures).to_s + "\n"
529526
unless other_names.empty?
530527
output_str << "Legend:\n"
531528
other_names.each do |name|

test/benchmark_runner_test.rb

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -9,62 +9,6 @@
99
require 'yaml'
1010

1111
describe BenchmarkRunner do
12-
describe '.table_to_str' do
13-
it 'formats a simple table correctly' do
14-
table_data = [
15-
['bench', 'time (ms)', 'stddev (%)'],
16-
['fib', '100.5', '2.3'],
17-
['loop', '50.2', '1.1']
18-
]
19-
format = ['%s', '%s', '%s']
20-
failures = {}
21-
22-
result = BenchmarkRunner.table_to_str(table_data, format, failures)
23-
24-
assert_equal <<~TABLE, result
25-
----- --------- ----------
26-
bench time (ms) stddev (%)
27-
fib 100.5 2.3
28-
loop 50.2 1.1
29-
----- --------- ----------
30-
TABLE
31-
end
32-
33-
it 'includes failure rows when failures are present' do
34-
table_data = [
35-
['bench', 'time (ms)'],
36-
['fib', '100.5']
37-
]
38-
format = ['%s', '%s']
39-
failures = { 'ruby' => { 'broken_bench' => 1 } }
40-
41-
result = BenchmarkRunner.table_to_str(table_data, format, failures)
42-
43-
assert_equal <<~TABLE, result
44-
------------ ---------
45-
bench time (ms)
46-
broken_bench N/A
47-
fib 100.5
48-
------------ ---------
49-
TABLE
50-
end
51-
52-
it 'handles empty failures hash' do
53-
table_data = [['bench'], ['fib']]
54-
format = ['%s']
55-
failures = {}
56-
57-
result = BenchmarkRunner.table_to_str(table_data, format, failures)
58-
59-
assert_equal <<~TABLE, result
60-
-----
61-
bench
62-
fib
63-
-----
64-
TABLE
65-
end
66-
end
67-
6812
describe '.free_file_no' do
6913
it 'returns 1 when no files exist' do
7014
Dir.mktmpdir do |dir|

test/table_formatter_test.rb

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
require_relative 'test_helper'
2+
require_relative '../lib/table_formatter'
3+
4+
describe TableFormatter do
5+
describe '#to_s' do
6+
it 'formats a simple table correctly' do
7+
table_data = [
8+
['bench', 'time (ms)', 'stddev (%)'],
9+
['fib', '100.5', '2.3'],
10+
['loop', '50.2', '1.1']
11+
]
12+
format = ['%s', '%s', '%s']
13+
failures = {}
14+
15+
result = TableFormatter.new(table_data, format, failures).to_s
16+
17+
assert_equal <<~TABLE, result
18+
----- --------- ----------
19+
bench time (ms) stddev (%)
20+
fib 100.5 2.3
21+
loop 50.2 1.1
22+
----- --------- ----------
23+
TABLE
24+
end
25+
26+
it 'includes failure rows when failures are present' do
27+
table_data = [
28+
['bench', 'time (ms)'],
29+
['fib', '100.5']
30+
]
31+
format = ['%s', '%s']
32+
failures = { 'ruby' => { 'broken_bench' => 1 } }
33+
34+
result = TableFormatter.new(table_data, format, failures).to_s
35+
36+
assert_equal <<~TABLE, result
37+
------------ ---------
38+
bench time (ms)
39+
broken_bench N/A
40+
fib 100.5
41+
------------ ---------
42+
TABLE
43+
end
44+
45+
it 'handles empty failures hash' do
46+
table_data = [['bench'], ['fib']]
47+
format = ['%s']
48+
failures = {}
49+
50+
result = TableFormatter.new(table_data, format, failures).to_s
51+
52+
assert_equal <<~TABLE, result
53+
-----
54+
bench
55+
fib
56+
-----
57+
TABLE
58+
end
59+
60+
it 'handles empty failures hash' do
61+
table_data = [['bench'], ['fib']]
62+
format = ['%s']
63+
failures = {}
64+
65+
result = TableFormatter.new(table_data, format, failures).to_s
66+
refute_includes result, 'N/A'
67+
end
68+
69+
it 'handles multiple failures from different executables' do
70+
table_data = [
71+
['bench', 'time (ms)'],
72+
['fib', '100.5']
73+
]
74+
format = ['%s', '%s']
75+
failures = {
76+
'ruby' => { 'broken_bench' => 1 },
77+
'ruby-yjit' => { 'another_broken' => 1 }
78+
}
79+
80+
result = TableFormatter.new(table_data, format, failures).to_s
81+
82+
assert_includes result, 'broken_bench'
83+
assert_includes result, 'another_broken'
84+
# Count N/A occurrences - should have 2 (one for each failed benchmark)
85+
assert_equal 2, result.scan(/N\/A/).count
86+
end
87+
88+
it 'removes trailing spaces from last column' do
89+
table_data = [
90+
['bench', 'time (ms)', 'stddev (%)'],
91+
['fib', '100.5', '2.3']
92+
]
93+
format = ['%s', '%s', '%s']
94+
failures = {}
95+
96+
result = TableFormatter.new(table_data, format, failures).to_s
97+
lines = result.lines
98+
99+
# No line should have trailing spaces before the newline
100+
lines.each do |line|
101+
refute_match(/ \n\z/, line, "Line should not have trailing spaces: #{line.inspect}")
102+
end
103+
end
104+
105+
it 'applies format strings correctly' do
106+
table_data = [
107+
['bench', 'time'],
108+
['fib', 123.456]
109+
]
110+
format = ['%s', '%.1f']
111+
failures = {}
112+
113+
result = TableFormatter.new(table_data, format, failures).to_s
114+
115+
assert_includes result, '123.5' # Should round to 1 decimal
116+
refute_includes result, '123.456'
117+
end
118+
end
119+
end

0 commit comments

Comments
 (0)