serialbench 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/benchmark.yml +181 -30
  3. data/.github/workflows/ci.yml +3 -3
  4. data/.github/workflows/docker.yml +272 -0
  5. data/.github/workflows/rake.yml +15 -0
  6. data/.github/workflows/release.yml +25 -0
  7. data/Gemfile +6 -30
  8. data/README.adoc +381 -415
  9. data/Rakefile +0 -55
  10. data/config/benchmarks/full.yml +29 -0
  11. data/config/benchmarks/short.yml +26 -0
  12. data/config/environments/asdf-ruby-3.2.yml +8 -0
  13. data/config/environments/asdf-ruby-3.3.yml +8 -0
  14. data/config/environments/docker-ruby-3.0.yml +9 -0
  15. data/config/environments/docker-ruby-3.1.yml +9 -0
  16. data/config/environments/docker-ruby-3.2.yml +9 -0
  17. data/config/environments/docker-ruby-3.3.yml +9 -0
  18. data/config/environments/docker-ruby-3.4.yml +9 -0
  19. data/docker/Dockerfile.alpine +33 -0
  20. data/docker/Dockerfile.ubuntu +32 -0
  21. data/docker/README.md +214 -0
  22. data/exe/serialbench +1 -1
  23. data/lib/serialbench/benchmark_runner.rb +270 -350
  24. data/lib/serialbench/cli/base_cli.rb +51 -0
  25. data/lib/serialbench/cli/benchmark_cli.rb +380 -0
  26. data/lib/serialbench/cli/environment_cli.rb +181 -0
  27. data/lib/serialbench/cli/resultset_cli.rb +215 -0
  28. data/lib/serialbench/cli/ruby_build_cli.rb +238 -0
  29. data/lib/serialbench/cli.rb +59 -410
  30. data/lib/serialbench/config_manager.rb +140 -0
  31. data/lib/serialbench/models/benchmark_config.rb +63 -0
  32. data/lib/serialbench/models/benchmark_result.rb +45 -0
  33. data/lib/serialbench/models/environment_config.rb +71 -0
  34. data/lib/serialbench/models/platform.rb +59 -0
  35. data/lib/serialbench/models/result.rb +53 -0
  36. data/lib/serialbench/models/result_set.rb +71 -0
  37. data/lib/serialbench/models/result_store.rb +108 -0
  38. data/lib/serialbench/models.rb +54 -0
  39. data/lib/serialbench/ruby_build_manager.rb +153 -0
  40. data/lib/serialbench/runners/asdf_runner.rb +296 -0
  41. data/lib/serialbench/runners/base.rb +32 -0
  42. data/lib/serialbench/runners/docker_runner.rb +142 -0
  43. data/lib/serialbench/serializers/base_serializer.rb +8 -16
  44. data/lib/serialbench/serializers/json/base_json_serializer.rb +4 -4
  45. data/lib/serialbench/serializers/json/json_serializer.rb +0 -2
  46. data/lib/serialbench/serializers/json/oj_serializer.rb +0 -2
  47. data/lib/serialbench/serializers/json/rapidjson_serializer.rb +50 -0
  48. data/lib/serialbench/serializers/json/yajl_serializer.rb +6 -4
  49. data/lib/serialbench/serializers/toml/base_toml_serializer.rb +5 -3
  50. data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +0 -2
  51. data/lib/serialbench/serializers/toml/tomlib_serializer.rb +0 -2
  52. data/lib/serialbench/serializers/toml/tomlrb_serializer.rb +56 -0
  53. data/lib/serialbench/serializers/xml/base_xml_serializer.rb +4 -9
  54. data/lib/serialbench/serializers/xml/libxml_serializer.rb +0 -2
  55. data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +21 -5
  56. data/lib/serialbench/serializers/xml/oga_serializer.rb +0 -2
  57. data/lib/serialbench/serializers/xml/ox_serializer.rb +0 -2
  58. data/lib/serialbench/serializers/xml/rexml_serializer.rb +32 -4
  59. data/lib/serialbench/serializers/yaml/base_yaml_serializer.rb +59 -0
  60. data/lib/serialbench/serializers/yaml/psych_serializer.rb +54 -0
  61. data/lib/serialbench/serializers/yaml/syck_serializer.rb +102 -0
  62. data/lib/serialbench/serializers.rb +34 -6
  63. data/lib/serialbench/site_generator.rb +105 -0
  64. data/lib/serialbench/templates/assets/css/benchmark_report.css +535 -0
  65. data/lib/serialbench/templates/assets/css/format_based.css +526 -0
  66. data/lib/serialbench/templates/assets/css/themes.css +588 -0
  67. data/lib/serialbench/templates/assets/js/chart_helpers.js +381 -0
  68. data/lib/serialbench/templates/assets/js/dashboard.js +796 -0
  69. data/lib/serialbench/templates/assets/js/navigation.js +142 -0
  70. data/lib/serialbench/templates/base.liquid +49 -0
  71. data/lib/serialbench/templates/format_based.liquid +279 -0
  72. data/lib/serialbench/templates/partials/chart_section.liquid +4 -0
  73. data/lib/serialbench/version.rb +1 -1
  74. data/lib/serialbench.rb +2 -31
  75. data/serialbench.gemspec +28 -17
  76. metadata +192 -55
  77. data/lib/serialbench/chart_generator.rb +0 -821
  78. data/lib/serialbench/result_formatter.rb +0 -182
  79. data/lib/serialbench/result_merger.rb +0 -1201
  80. data/lib/serialbench/serializers/xml/base_parser.rb +0 -69
  81. data/lib/serialbench/serializers/xml/libxml_parser.rb +0 -98
  82. data/lib/serialbench/serializers/xml/nokogiri_parser.rb +0 -111
  83. data/lib/serialbench/serializers/xml/oga_parser.rb +0 -85
  84. data/lib/serialbench/serializers/xml/ox_parser.rb +0 -64
  85. data/lib/serialbench/serializers/xml/rexml_parser.rb +0 -129
@@ -1,438 +1,87 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thor'
4
- require 'json'
5
- require 'yaml'
6
- require 'fileutils'
4
+ require_relative 'cli/base_cli'
5
+ require_relative 'cli/environment_cli'
6
+ require_relative 'cli/benchmark_cli'
7
+ require_relative 'cli/resultset_cli'
8
+ require_relative 'cli/ruby_build_cli'
7
9
 
8
10
  module Serialbench
9
- # Thor-based command line interface for SerialBench
10
- class Cli < Thor
11
- include Thor::Actions
11
+ # Main CLI entry point for the new object-oriented command structure
12
+ class CLI < Serialbench::Cli::BaseCli
13
+ desc 'environment SUBCOMMAND', 'Manage benchmark environments'
14
+ subcommand 'environment', Serialbench::Cli::EnvironmentCli
12
15
 
13
- desc 'benchmark', 'Run serialization benchmarks'
14
- long_desc <<~DESC
15
- Run the complete benchmark suite for all available serialization libraries.
16
+ desc 'benchmark SUBCOMMAND', 'Manage individual benchmark runs'
17
+ subcommand 'benchmark', Serialbench::Cli::BenchmarkCli
16
18
 
17
- This command will test parsing, generation, streaming, and memory usage
18
- across XML, JSON, and TOML formats using all available libraries.
19
- DESC
20
- option :formats, type: :array, default: %w[xml json toml],
21
- desc: 'Formats to benchmark (xml, json, toml)'
22
- option :output_format, type: :string, default: 'all',
23
- desc: 'Output format: all, json, yaml, html'
24
- option :parsing_only, type: :boolean, default: false,
25
- desc: 'Run only parsing benchmarks'
26
- option :generation_only, type: :boolean, default: false,
27
- desc: 'Run only generation benchmarks'
28
- option :streaming_only, type: :boolean, default: false,
29
- desc: 'Run only streaming benchmarks'
30
- option :memory_only, type: :boolean, default: false,
31
- desc: 'Run only memory usage benchmarks'
32
- option :iterations, type: :numeric, default: 10,
33
- desc: 'Number of benchmark iterations'
34
- option :warmup, type: :numeric, default: 3,
35
- desc: 'Number of warmup iterations'
36
- def benchmark
37
- say 'SerialBench - Comprehensive Serialization Performance Tests', :green
38
- say '=' * 70, :green
19
+ desc 'resultset SUBCOMMAND', 'Manage benchmark resultsets (collections of runs)'
20
+ subcommand 'resultset', Serialbench::Cli::ResultsetCli
39
21
 
40
- # Validate formats
41
- valid_formats = %w[xml json toml]
42
- invalid_formats = options[:formats] - valid_formats
43
- unless invalid_formats.empty?
44
- say "Invalid formats: #{invalid_formats.join(', ')}", :red
45
- say "Valid formats: #{valid_formats.join(', ')}", :yellow
46
- exit 1
47
- end
48
-
49
- # Convert format strings to symbols
50
- formats = options[:formats].map(&:to_sym)
51
-
52
- # Show available serializers
53
- show_available_serializers(formats)
54
-
55
- # Run benchmarks
56
- runner_options = {
57
- formats: formats,
58
- iterations: options[:iterations],
59
- warmup: options[:warmup]
60
- }
22
+ desc 'ruby_build SUBCOMMAND', 'Manage Ruby-Build definitions for validation'
23
+ subcommand 'ruby_build', Serialbench::Cli::RubyBuildCli
61
24
 
62
- runner = Serialbench::BenchmarkRunner.new(**runner_options)
63
-
64
- begin
65
- results = run_selected_benchmarks(runner)
66
- save_results(results)
67
- show_summary(results) unless %w[json yaml].include?(options[:output_format])
68
- rescue StandardError => e
69
- say "Error running benchmarks: #{e.message}", :red
70
- say e.backtrace.first(5).join("\n"), :red if ENV['DEBUG']
71
- exit 1
72
- end
25
+ desc 'version', 'Show Serialbench version'
26
+ def self.version
27
+ puts "Serialbench version #{Serialbench::VERSION}"
73
28
  end
74
29
 
75
- desc 'list', 'List available serializers'
76
- long_desc <<~DESC
77
- Display all available serialization libraries grouped by format.
78
-
79
- Shows which libraries are installed and available for benchmarking,
80
- along with their versions.
81
- DESC
82
- option :format, type: :string, desc: 'Show only serializers for specific format'
83
- def list
84
- say 'Available Serializers', :green
85
- say '=' * 30, :green
86
-
87
- if options[:format]
88
- format_sym = options[:format].to_sym
89
- serializers = Serialbench::Serializers.available_for_format(format_sym)
90
-
91
- if serializers.empty?
92
- say "No available serializers for format: #{options[:format]}", :yellow
93
- else
94
- show_serializers_for_format(format_sym, serializers)
95
- end
30
+ desc 'help [COMMAND]', 'Show help for commands'
31
+ def help(command = nil)
32
+ if command
33
+ super(command)
96
34
  else
97
- %i[xml json toml].each do |format|
98
- serializers = Serialbench::Serializers.available_for_format(format)
99
- next if serializers.empty?
100
-
101
- show_serializers_for_format(format, serializers)
102
- say ''
103
- end
104
- end
105
- end
106
-
107
- desc 'version', 'Show SerialBench version'
108
- def version
109
- say "SerialBench version #{Serialbench::VERSION}", :green
110
- end
111
-
112
- desc 'merge_results INPUT_DIRS... OUTPUT_DIR', 'Merge benchmark results from multiple runs'
113
- long_desc <<~DESC
114
- Merge benchmark results from multiple Ruby versions or different environments.
115
-
116
- INPUT_DIRS should contain results.json files from different benchmark runs.
117
- OUTPUT_DIR will contain the merged results and comparative reports.
118
-
119
- Example:
120
- serialbench merge_results ruby-3.0/results ruby-3.1/results ruby-3.2/results merged_output/
121
- DESC
122
- def merge_results(*args)
123
- if args.length < 2
124
- say 'Error: Need at least one input directory and one output directory', :red
125
- say 'Usage: serialbench merge_results INPUT_DIRS... OUTPUT_DIR', :yellow
126
- exit 1
127
- end
128
-
129
- output_dir = args.pop
130
- input_dirs = args
131
-
132
- say "Merging benchmark results from #{input_dirs.length} directories to #{output_dir}", :green
133
-
134
- begin
135
- merger = Serialbench::ResultMerger.new
136
- merged_file = merger.merge_directories(input_dirs, output_dir)
137
- say "Results merged successfully to: #{merged_file}", :green
138
- rescue StandardError => e
139
- say "Error merging results: #{e.message}", :red
140
- exit 1
141
- end
142
- end
143
-
144
- desc 'github_pages INPUT_DIRS... OUTPUT_DIR', 'Generate GitHub Pages HTML from multiple benchmark runs'
145
- long_desc <<~DESC
146
- Merge benchmark results from multiple Ruby versions and generate a GitHub Pages compatible HTML report.
147
-
148
- INPUT_DIRS should contain results.json files from different benchmark runs.
149
- OUTPUT_DIR will contain index.html and styles.css ready for GitHub Pages deployment.
35
+ puts <<~HELP
36
+ Serialbench - Benchmarking Framework for Ruby Serialization Libraries
150
37
 
151
- This command combines merge_results and HTML generation in one step.
38
+ USAGE:
39
+ serialbench COMMAND [SUBCOMMAND] [OPTIONS]
152
40
 
153
- Example:
154
- serialbench github_pages ruby-3.0/results ruby-3.1/results ruby-3.2/results docs/
155
- DESC
156
- def github_pages(*args)
157
- if args.length < 2
158
- say 'Error: Need at least one input directory and one output directory', :red
159
- say 'Usage: serialbench github_pages INPUT_DIRS... OUTPUT_DIR', :yellow
160
- exit 1
161
- end
41
+ COMMANDS:
42
+ environment Manage benchmark environments (Docker, ASDF, Local)
43
+ benchmark Manage individual benchmark runs
44
+ resultset Manage benchmark resultsets (collections of runs)
45
+ ruby-build Manage Ruby-Build definitions for validation
46
+ version Show version information
47
+ help Show this help message
162
48
 
163
- output_dir = args.pop
164
- input_dirs = args
49
+ EXAMPLES:
50
+ # Create a Docker environment
51
+ serialbench environment new docker-test docker
165
52
 
166
- say "Generating GitHub Pages from #{input_dirs.length} benchmark directories", :green
53
+ # Run multi-environment benchmarks
54
+ serialbench environment multi-execute asdf --config=serialbench-asdf.yml
55
+ serialbench environment multi-execute docker --config=serialbench-docker.yml
167
56
 
168
- begin
169
- merger = Serialbench::ResultMerger.new
57
+ # Create and execute a benchmark
58
+ serialbench benchmark create my-benchmark
59
+ serialbench benchmark execute my-benchmark.yml
170
60
 
171
- # Merge results
172
- say 'Step 1: Merging benchmark results...', :yellow
173
- merger.merge_directories(input_dirs, output_dir)
61
+ # Create a result set for comparison
62
+ serialbench resultset create comparison-set
63
+ serialbench resultset add-result comparison-set results/my-benchmark
174
64
 
175
- # Generate GitHub Pages HTML
176
- say 'Step 2: Generating GitHub Pages HTML...', :yellow
177
- files = merger.generate_github_pages_html(output_dir)
65
+ # Generate static sites
66
+ serialbench benchmark build-site results/my-benchmark
67
+ serialbench resultset build-site resultsets/comparison-set
178
68
 
179
- say 'GitHub Pages generated successfully!', :green
180
- say 'Files created:', :cyan
181
- say " HTML: #{files[:html]}", :white
182
- say " CSS: #{files[:css]}", :white
183
- say '', :white
184
- say 'To deploy to GitHub Pages:', :cyan
185
- say '1. Commit and push the generated files to your repository', :white
186
- say '2. Enable GitHub Pages in repository settings', :white
187
- say '3. Set source to the branch containing these files', :white
188
- rescue StandardError => e
189
- say "Error generating GitHub Pages: #{e.message}", :red
190
- exit 1
69
+ For detailed help on any command, use:
70
+ serialbench COMMAND help
71
+ HELP
191
72
  end
192
73
  end
193
74
 
194
- desc 'generate_reports DATA_FILE', 'Generate reports from benchmark data'
195
- long_desc <<~DESC
196
- Generate HTML and AsciiDoc reports from existing benchmark data.
197
-
198
- DATA_FILE should be a JSON file containing benchmark results.
199
- DESC
200
- def generate_reports(data_file)
201
- say "Generating reports from data in #{data_file}", :green
202
-
203
- unless File.exist?(data_file)
204
- say "Data file does not exist: #{data_file}", :red
205
- exit 1
206
- end
207
-
208
- begin
209
- Serialbench.generate_reports_from_data(data_file)
210
- say 'Reports generated successfully!', :green
211
- rescue StandardError => e
212
- say "Error generating reports: #{e.message}", :red
213
- exit 1
214
- end
215
- end
216
-
217
- private
218
-
219
- def show_available_serializers(formats)
220
- say "\nAvailable serializers:", :cyan
221
-
222
- formats.each do |format|
223
- serializers = Serialbench::Serializers.available_for_format(format)
224
- next if serializers.empty?
225
-
226
- serializer_names = serializers.map do |serializer_class|
227
- serializer = serializer_class.new
228
- "#{serializer.name} v#{serializer.version}"
229
- end
230
-
231
- say " #{format.upcase}: #{serializer_names.join(', ')}", :white
232
- end
233
-
234
- say "\nTest data sizes: small, medium, large", :cyan
235
- say ''
236
- end
237
-
238
- def show_serializers_for_format(format, serializers)
239
- say "#{format.upcase}:", :cyan
240
-
241
- serializers.each do |serializer_class|
242
- serializer = serializer_class.new
243
- features = []
244
- features << 'streaming' if serializer.supports_streaming?
245
- features << 'built-in' if %w[json rexml].include?(serializer.name)
246
-
247
- feature_text = features.empty? ? '' : " (#{features.join(', ')})"
248
- say " ✓ #{serializer.name} v#{serializer.version}#{feature_text}", :green
249
- end
250
- end
251
-
252
- def run_selected_benchmarks(runner)
253
- results = { environment: runner.environment_info }
254
-
255
- if options[:parsing_only]
256
- say 'Running parsing benchmarks...', :yellow
257
- results[:parsing] = runner.run_parsing_benchmarks
258
- elsif options[:generation_only]
259
- say 'Running generation benchmarks...', :yellow
260
- results[:generation] = runner.run_generation_benchmarks
261
- elsif options[:streaming_only]
262
- say 'Running streaming benchmarks...', :yellow
263
- results[:streaming] = runner.run_streaming_benchmarks
264
- elsif options[:memory_only]
265
- say 'Running memory benchmarks...', :yellow
266
- results[:memory_usage] = runner.run_memory_benchmarks
267
- else
268
- say 'Running all benchmarks...', :yellow
269
- results = runner.run_all_benchmarks
270
- end
271
-
272
- results
75
+ # Handle unknown commands gracefully
76
+ def method_missing(method_name, *args)
77
+ puts "Unknown command: #{method_name}"
78
+ puts ''
79
+ help
80
+ exit 1
273
81
  end
274
82
 
275
- def save_results(results)
276
- case options[:output_format]
277
- when 'json'
278
- save_json_results(results)
279
- when 'yaml'
280
- save_yaml_results(results)
281
- when 'html'
282
- generate_html_reports(results)
283
- else
284
- # Generate all formats
285
- save_json_results(results)
286
- save_yaml_results(results)
287
- generate_html_reports(results)
288
- end
289
-
290
- show_generated_files
291
- end
292
-
293
- def save_json_results(results)
294
- FileUtils.mkdir_p('results/data')
295
-
296
- # Add Ruby version to results
297
- results[:ruby_version] = RUBY_VERSION
298
- results[:ruby_platform] = RUBY_PLATFORM
299
- results[:timestamp] = Time.now.iso8601
300
-
301
- File.write('results/data/results.json', JSON.pretty_generate(results))
302
- say 'JSON results saved to: results/data/results.json', :green
303
- end
304
-
305
- def save_yaml_results(results)
306
- FileUtils.mkdir_p('results/data')
307
-
308
- # Add Ruby version to results
309
- results[:ruby_version] = RUBY_VERSION
310
- results[:ruby_platform] = RUBY_PLATFORM
311
- results[:timestamp] = Time.now.iso8601
312
-
313
- File.write('results/data/results.yaml', results.to_yaml)
314
- say 'YAML results saved to: results/data/results.yaml', :green
315
- end
316
-
317
- def generate_html_reports(results)
318
- say 'Generating reports...', :yellow
319
- report_files = Serialbench.generate_reports(results)
320
-
321
- say 'Reports generated:', :green
322
- say " HTML: #{report_files[:html]}", :white
323
- say " CSS: #{report_files[:css]}", :white
324
- end
325
-
326
- def show_generated_files
327
- case options[:output_format]
328
- when 'json'
329
- say 'Files generated:', :cyan
330
- say ' JSON: results/data/results.json', :white
331
- when 'yaml'
332
- say 'Files generated:', :cyan
333
- say ' YAML: results/data/results.yaml', :white
334
- when 'html'
335
- say 'Files generated:', :cyan
336
- say ' HTML: results/reports/benchmark_report.html', :white
337
- say ' Charts: results/charts/*.svg', :white
338
- else
339
- say 'Files generated:', :cyan
340
- say ' JSON: results/data/results.json', :white
341
- say ' YAML: results/data/results.yaml', :white
342
- say ' HTML: results/reports/benchmark_report.html', :white
343
- say ' Charts: results/charts/*.svg', :white
344
- end
345
- end
346
-
347
- def show_summary(results)
348
- return unless results[:parsing] || results[:generation]
349
-
350
- say "\n" + '=' * 50, :green
351
- say 'BENCHMARK SUMMARY', :green
352
- say '=' * 50, :green
353
-
354
- show_parsing_summary(results[:parsing]) if results[:parsing]
355
-
356
- show_generation_summary(results[:generation]) if results[:generation]
357
-
358
- return unless results[:memory_usage]
359
-
360
- show_memory_summary(results[:memory_usage])
361
- end
362
-
363
- def show_parsing_summary(parsing_results)
364
- say "\nParsing Performance (operations/second):", :cyan
365
-
366
- %i[small medium large].each do |size|
367
- next unless parsing_results[size]
368
-
369
- say "\n #{size.capitalize} files:", :yellow
370
-
371
- # Flatten the nested structure and sort by performance
372
- flattened_results = []
373
- parsing_results[size].each do |format, serializers|
374
- serializers.each do |serializer_name, data|
375
- flattened_results << ["#{format}/#{serializer_name}", data]
376
- end
377
- end
378
-
379
- sorted_results = flattened_results.sort_by { |_, data| -data[:iterations_per_second] }
380
-
381
- sorted_results.each do |serializer_name, data|
382
- ops_per_sec = data[:iterations_per_second].round(2)
383
- say " #{serializer_name}: #{ops_per_sec} ops/sec", :white
384
- end
385
- end
386
- end
387
-
388
- def show_generation_summary(generation_results)
389
- say "\nGeneration Performance (operations/second):", :cyan
390
-
391
- %i[small medium large].each do |size|
392
- next unless generation_results[size]
393
-
394
- say "\n #{size.capitalize} files:", :yellow
395
-
396
- # Flatten the nested structure and sort by performance
397
- flattened_results = []
398
- generation_results[size].each do |format, serializers|
399
- serializers.each do |serializer_name, data|
400
- flattened_results << ["#{format}/#{serializer_name}", data]
401
- end
402
- end
403
-
404
- sorted_results = flattened_results.sort_by { |_, data| -data[:iterations_per_second] }
405
-
406
- sorted_results.each do |serializer_name, data|
407
- ops_per_sec = data[:iterations_per_second].round(2)
408
- say " #{serializer_name}: #{ops_per_sec} ops/sec", :white
409
- end
410
- end
411
- end
412
-
413
- def show_memory_summary(memory_results)
414
- say "\nMemory Usage (MB):", :cyan
415
-
416
- %i[small medium large].each do |size|
417
- next unless memory_results[size]
418
-
419
- say "\n #{size.capitalize} files:", :yellow
420
-
421
- # Flatten the nested structure and sort by memory usage (ascending)
422
- flattened_results = []
423
- memory_results[size].each do |format, serializers|
424
- serializers.each do |serializer_name, data|
425
- flattened_results << ["#{format}/#{serializer_name}", data]
426
- end
427
- end
428
-
429
- sorted_results = flattened_results.sort_by { |_, data| data[:allocated_memory] }
430
-
431
- sorted_results.each do |serializer_name, data|
432
- memory_mb = (data[:allocated_memory] / 1024.0 / 1024.0).round(2)
433
- say " #{serializer_name}: #{memory_mb} MB", :white
434
- end
435
- end
83
+ def respond_to_missing?(method_name, include_private = false)
84
+ false
436
85
  end
437
86
  end
438
87
  end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'ostruct'
5
+ require_relative 'schema_validator'
6
+
7
+ module Serialbench
8
+ # Manages configuration loading and validation for Serialbench
9
+ class ConfigManager
10
+ class ConfigurationError < StandardError; end
11
+
12
+ SCHEMA_PATH = File.join(__dir__, '../../docs/serialbench_config_schema.yaml')
13
+
14
+ # Load and validate configuration from file
15
+ def self.load_and_validate(config_path)
16
+ new.load_and_validate(config_path)
17
+ end
18
+
19
+ def initialize
20
+ @validator = SchemaValidator.new
21
+ end
22
+
23
+ # Load configuration file and validate against schema
24
+ def load_and_validate(config_path)
25
+ raise ConfigurationError, "Configuration file not found: #{config_path}" unless File.exist?(config_path)
26
+
27
+ begin
28
+ config_data = YAML.load_file(config_path)
29
+ rescue Psych::SyntaxError => e
30
+ raise ConfigurationError, "Invalid YAML syntax in #{config_path}: #{e.message}"
31
+ rescue StandardError => e
32
+ raise ConfigurationError, "Error reading configuration file #{config_path}: #{e.message}"
33
+ end
34
+
35
+ validate_config(config_data, config_path)
36
+ normalize_config(config_data)
37
+ end
38
+
39
+ private
40
+
41
+ # Validate configuration against schema
42
+ def validate_config(config_data, config_path)
43
+ # For now, perform basic validation since we don't have a specific config schema validator
44
+ validate_basic_config(config_data)
45
+ end
46
+
47
+ # Load the configuration schema
48
+ def load_schema
49
+ unless File.exist?(SCHEMA_PATH)
50
+ raise ConfigurationError, "Configuration schema not found: #{SCHEMA_PATH}"
51
+ end
52
+
53
+ begin
54
+ YAML.load_file(SCHEMA_PATH)
55
+ rescue StandardError => e
56
+ raise ConfigurationError, "Error loading configuration schema: #{e.message}"
57
+ end
58
+ end
59
+
60
+ # Normalize and convert configuration to structured object
61
+ def normalize_config(config_data)
62
+ config = OpenStruct.new(config_data)
63
+
64
+ # Apply defaults
65
+ config.output_dir ||= 'benchmark-results'
66
+ config.benchmark_config ||= 'config/full.yml'
67
+ config.auto_install = true if config.auto_install.nil?
68
+
69
+ # Validate runtime-specific requirements
70
+ case config.runtime
71
+ when 'docker'
72
+ validate_docker_config(config)
73
+ when 'asdf'
74
+ validate_asdf_config(config)
75
+ else
76
+ raise ConfigurationError, "Unknown runtime: #{config.runtime}"
77
+ end
78
+
79
+ config
80
+ end
81
+
82
+ # Validate Docker-specific configuration
83
+ def validate_docker_config(config)
84
+ unless config.image_variants && !config.image_variants.empty?
85
+ raise ConfigurationError, "Docker runtime requires 'image_variants' to be specified"
86
+ end
87
+
88
+ invalid_variants = config.image_variants - %w[slim alpine]
89
+ unless invalid_variants.empty?
90
+ raise ConfigurationError, "Invalid image variants: #{invalid_variants.join(', ')}. Valid variants: slim, alpine"
91
+ end
92
+ end
93
+
94
+ # Validate ASDF-specific configuration
95
+ def validate_asdf_config(config)
96
+ # Check if ASDF is available
97
+ unless command_available?('asdf')
98
+ raise ConfigurationError, "ASDF is not installed or not in PATH. Please install ASDF to use asdf runtime."
99
+ end
100
+
101
+ # Validate Ruby version format for ASDF (should include patch version)
102
+ config.ruby_versions.each do |version|
103
+ unless version.match?(/^\d+\.\d+\.\d+$/)
104
+ raise ConfigurationError, "ASDF runtime requires full version numbers (e.g., '3.2.8'), got: #{version}"
105
+ end
106
+ end
107
+ end
108
+
109
+ # Basic configuration validation
110
+ def validate_basic_config(config_data)
111
+ # Check required fields
112
+ required_fields = %w[runtime ruby_versions output_dir benchmark_config]
113
+ required_fields.each do |field|
114
+ unless config_data.key?(field)
115
+ raise ConfigurationError, "Missing required field: #{field}"
116
+ end
117
+ end
118
+
119
+ # Validate runtime
120
+ valid_runtimes = %w[docker asdf]
121
+ unless valid_runtimes.include?(config_data['runtime'])
122
+ raise ConfigurationError, "Invalid runtime: #{config_data['runtime']}. Valid runtimes: #{valid_runtimes.join(', ')}"
123
+ end
124
+
125
+ # Validate ruby_versions is an array
126
+ unless config_data['ruby_versions'].is_a?(Array)
127
+ raise ConfigurationError, "ruby_versions must be an array"
128
+ end
129
+
130
+ if config_data['ruby_versions'].empty?
131
+ raise ConfigurationError, "ruby_versions cannot be empty"
132
+ end
133
+ end
134
+
135
+ # Check if a command is available in PATH
136
+ def command_available?(command)
137
+ system("which #{command} > /dev/null 2>&1")
138
+ end
139
+ end
140
+ end