serialbench 0.1.2 → 0.2.0

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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/benchmark.yml +294 -229
  3. data/.github/workflows/rake.yml +11 -0
  4. data/.github/workflows/windows-debug.yml +171 -0
  5. data/.gitignore +32 -0
  6. data/.rubocop.yml +1 -0
  7. data/.rubocop_todo.yml +274 -0
  8. data/Gemfile +15 -1
  9. data/README.adoc +41 -0
  10. data/data/schemas/result.yml +29 -0
  11. data/docs/PLATFORM_VALIDATION_FIX.md +79 -0
  12. data/docs/SYCK_YAML_FIX.md +91 -0
  13. data/docs/WEBSITE_COMPLETION_PLAN.md +440 -0
  14. data/docs/WINDOWS_LIBXML_FIX.md +136 -0
  15. data/docs/WINDOWS_SETUP.md +122 -0
  16. data/lib/serialbench/benchmark_runner.rb +3 -3
  17. data/lib/serialbench/cli/benchmark_cli.rb +74 -1
  18. data/lib/serialbench/cli/environment_cli.rb +3 -3
  19. data/lib/serialbench/cli/resultset_cli.rb +112 -26
  20. data/lib/serialbench/cli/ruby_build_cli.rb +75 -88
  21. data/lib/serialbench/cli/validate_cli.rb +88 -0
  22. data/lib/serialbench/cli.rb +6 -2
  23. data/lib/serialbench/config_manager.rb +15 -26
  24. data/lib/serialbench/models/benchmark_config.rb +12 -0
  25. data/lib/serialbench/models/benchmark_result.rb +39 -3
  26. data/lib/serialbench/models/environment_config.rb +3 -2
  27. data/lib/serialbench/models/platform.rb +61 -6
  28. data/lib/serialbench/models/result.rb +28 -1
  29. data/lib/serialbench/models/result_set.rb +8 -0
  30. data/lib/serialbench/ruby_build_manager.rb +19 -23
  31. data/lib/serialbench/runners/asdf_runner.rb +1 -1
  32. data/lib/serialbench/runners/docker_runner.rb +2 -4
  33. data/lib/serialbench/runners/local_runner.rb +71 -0
  34. data/lib/serialbench/serializers/base_serializer.rb +1 -1
  35. data/lib/serialbench/serializers/json/rapidjson_serializer.rb +1 -5
  36. data/lib/serialbench/serializers/toml/base_toml_serializer.rb +0 -2
  37. data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +1 -1
  38. data/lib/serialbench/serializers/toml/tomlib_serializer.rb +1 -1
  39. data/lib/serialbench/serializers/xml/base_xml_serializer.rb +6 -1
  40. data/lib/serialbench/serializers/xml/leptris_serializer.rb +167 -0
  41. data/lib/serialbench/serializers/xml/libxml_serializer.rb +13 -8
  42. data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +11 -2
  43. data/lib/serialbench/serializers/xml/oga_serializer.rb +8 -8
  44. data/lib/serialbench/serializers/xml/ox_serializer.rb +2 -2
  45. data/lib/serialbench/serializers/xml/rexml_serializer.rb +3 -3
  46. data/lib/serialbench/serializers/yaml/base_yaml_serializer.rb +1 -3
  47. data/lib/serialbench/serializers/yaml/psych_serializer.rb +1 -5
  48. data/lib/serialbench/serializers/yaml/syck_serializer.rb +1 -1
  49. data/lib/serialbench/serializers.rb +5 -3
  50. data/lib/serialbench/site_generator.rb +234 -2
  51. data/lib/serialbench/templates/assets/css/format_based.css +1 -53
  52. data/lib/serialbench/templates/assets/css/themes.css +5 -4
  53. data/lib/serialbench/templates/assets/js/chart_helpers.js +44 -14
  54. data/lib/serialbench/templates/assets/js/dashboard.js +14 -15
  55. data/lib/serialbench/templates/format_based.liquid +480 -252
  56. data/lib/serialbench/version.rb +1 -1
  57. data/lib/serialbench/yaml_validator.rb +36 -0
  58. data/serialbench.gemspec +11 -2
  59. data/site/.gitignore +5 -0
  60. data/site/README.md +32 -0
  61. data/site/astro.config.mjs +12 -0
  62. data/site/package-lock.json +6160 -0
  63. data/site/package.json +22 -0
  64. data/site/public/favicon.svg +4 -0
  65. data/site/scripts/gen-sample-data.mjs +154 -0
  66. data/site/src/components/AvailabilityMatrix.astro +61 -0
  67. data/site/src/components/CalibratedLeaderboard.vue +134 -0
  68. data/site/src/components/CitationBox.vue +50 -0
  69. data/site/src/components/DashboardConsole.vue +193 -0
  70. data/site/src/components/FeaturesTable.astro +97 -0
  71. data/site/src/components/MemoryPanel.vue +44 -0
  72. data/site/src/components/OpsChart.vue +135 -0
  73. data/site/src/config.ts +18 -0
  74. data/site/src/layouts/Layout.astro +85 -0
  75. data/site/src/lib/channels.ts +24 -0
  76. data/site/src/lib/dashboard.ts +269 -0
  77. data/site/src/pages/index.astro +44 -0
  78. data/site/src/pages/library/[slug].astro +128 -0
  79. data/site/src/pages/methodology.astro +55 -0
  80. data/site/src/styles/global.css +103 -0
  81. data/site/tsconfig.json +5 -0
  82. metadata +58 -23
  83. data/.github/workflows/ci.yml +0 -74
  84. data/.github/workflows/docker.yml +0 -272
@@ -0,0 +1,122 @@
1
+ # Windows Setup Guide for Serialbench
2
+
3
+ This guide explains how to set up serialbench on Windows, particularly for installing the `libxml-ruby` gem which requires native libxml2 libraries.
4
+
5
+ ## Problem
6
+
7
+ The `libxml-ruby` gem requires native C libraries (libxml2) that need to be installed separately on Windows. Without these libraries, bundler will fail when trying to install the gem.
8
+
9
+ ## Solution
10
+
11
+ ### Option 1: Using Chocolatey (Recommended)
12
+
13
+ 1. Install [Chocolatey](https://chocolatey.org/install) if you haven't already:
14
+
15
+ ```powershell
16
+ Set-ExecutionPolicy Bypass -Scope Process -Force
17
+ [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
18
+ iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
19
+ ```
20
+
21
+ 2. Install libxml2 and required tools:
22
+
23
+ ```powershell
24
+ choco install -y libxml2
25
+ choco install -y pkgconfiglite
26
+ ```
27
+
28
+ 3. Set environment variables (you may need to adjust paths based on your installation):
29
+
30
+ ```powershell
31
+ # Find the actual libxml2 installation path
32
+ $libxmlPath = (Get-ChildItem "C:\ProgramData\chocolatey\lib\libxml2" -Recurse -Filter "libxml2.dll" | Select-Object -First 1).Directory.Parent.FullName
33
+
34
+ # Set PKG_CONFIG_PATH
35
+ $env:PKG_CONFIG_PATH = "$libxmlPath\lib\pkgconfig"
36
+ [System.Environment]::SetEnvironmentVariable('PKG_CONFIG_PATH', $env:PKG_CONFIG_PATH, 'User')
37
+
38
+ # Add to PATH
39
+ $env:PATH = "$libxmlPath\bin;$env:PATH"
40
+ $currentPath = [System.Environment]::GetEnvironmentVariable('PATH', 'User')
41
+ [System.Environment]::SetEnvironmentVariable('PATH', "$libxmlPath\bin;$currentPath", 'User')
42
+ ```
43
+
44
+ 4. Install Ruby dependencies:
45
+
46
+ ```powershell
47
+ bundle install
48
+ ```
49
+
50
+ ### Option 2: Using vcpkg
51
+
52
+ 1. Install vcpkg:
53
+
54
+ ```powershell
55
+ git clone https://github.com/Microsoft/vcpkg.git
56
+ cd vcpkg
57
+ .\bootstrap-vcpkg.bat
58
+ .\vcpkg integrate install
59
+ ```
60
+
61
+ 2. Install libxml2:
62
+
63
+ ```powershell
64
+ .\vcpkg install libxml2:x64-windows
65
+ ```
66
+
67
+ 3. Set environment variables:
68
+
69
+ ```powershell
70
+ $vcpkgPath = "C:\path\to\vcpkg\installed\x64-windows"
71
+ $env:PKG_CONFIG_PATH = "$vcpkgPath\lib\pkgconfig"
72
+ $env:PATH = "$vcpkgPath\bin;$env:PATH"
73
+ ```
74
+
75
+ ### Option 3: Skip libxml-ruby (Testing Only)
76
+
77
+ If you don't need to test the libxml serializer, you can temporarily exclude it:
78
+
79
+ ```ruby
80
+ # In Gemfile or when installing
81
+ bundle install --without libxml
82
+ ```
83
+
84
+ Or remove it from the gemspec temporarily (not recommended for CI).
85
+
86
+ ## Troubleshooting
87
+
88
+ ### Error: "Cannot find libxml2"
89
+
90
+ This means the libxml2 libraries are not in the expected locations. Try:
91
+
92
+ 1. Verify libxml2 is installed:
93
+ ```powershell
94
+ where libxml2.dll
95
+ ```
96
+
97
+ 2. Check pkg-config can find it:
98
+ ```powershell
99
+ pkg-config --libs libxml-2.0
100
+ ```
101
+
102
+ 3. Manually specify the path when installing the gem:
103
+ ```powershell
104
+ gem install libxml-ruby -- --with-xml2-dir="C:\path\to\libxml2"
105
+ ```
106
+
107
+ ### Error: "mkmf.log shows missing headers"
108
+
109
+ The development headers aren't found. Make sure you installed the complete libxml2 package including headers.
110
+
111
+ ## GitHub Actions CI
112
+
113
+ For CI/CD on GitHub Actions, see `.github/workflows/windows-setup.yml` for the complete setup that:
114
+
115
+ 1. Installs libxml2 via Chocolatey
116
+ 2. Configures environment paths
117
+ 3. Runs bundle install
118
+ 4. Executes tests
119
+
120
+ ## Local Development
121
+
122
+ For local development on Windows, follow the Chocolatey or vcpkg installation steps above. There is no automated Rake task - you must manually install libxml2 before running `bundle install`.
@@ -36,7 +36,7 @@ module Serialbench
36
36
  serializers: Serializers.information,
37
37
  parsing: run_parsing_benchmarks,
38
38
  generation: run_generation_benchmarks,
39
- memory_usage: run_memory_benchmarks,
39
+ memory: run_memory_benchmarks,
40
40
  streaming: run_streaming_benchmarks
41
41
  )
42
42
  end
@@ -221,7 +221,7 @@ module Serialbench
221
221
  city: "City #{i % 100}",
222
222
  preferences: {
223
223
  theme: i.even? ? 'dark' : 'light',
224
- notifications: i % 3 == 0
224
+ notifications: (i % 3).zero?
225
225
  }
226
226
  }
227
227
  }
@@ -244,7 +244,7 @@ module Serialbench
244
244
  data: {
245
245
  field1: "Value #{i}",
246
246
  field2: i * 2,
247
- field3: i % 100 == 0 ? 'special' : 'normal',
247
+ field3: (i % 100).zero? ? 'special' : 'normal',
248
248
  nested: [
249
249
  "Item #{i}-1",
250
250
  "Item #{i}-2",
@@ -242,6 +242,79 @@ module Serialbench
242
242
 
243
243
  private
244
244
 
245
+ def execute_local_benchmark(environment, config, benchmark_config_path)
246
+ say '🏠 Executing local benchmark', :green
247
+
248
+ runner = Serialbench::BenchmarkRunner.new(
249
+ environment_config: environment,
250
+ benchmark_config: config
251
+ )
252
+
253
+ # Run benchmarks
254
+ results = runner.run_all_benchmarks
255
+
256
+ platform = Serialbench::Models::Platform.current_local
257
+
258
+ metadata = Models::RunMetadata.new(
259
+ benchmark_config_path: benchmark_config_path,
260
+ environment_config_path: "config/environments/#{environment.name}.yml",
261
+ tags: [
262
+ 'local',
263
+ platform.os,
264
+ platform.arch,
265
+ "ruby-#{environment.ruby_build_tag}"
266
+ ]
267
+ )
268
+
269
+ # Create results directory
270
+ result_dir = "results/runs/#{environment.name}-results"
271
+ FileUtils.mkdir_p(result_dir)
272
+
273
+ # Save results to single YAML file with platform and metadata merged in
274
+ results_model = Models::Result.new(
275
+ platform: platform,
276
+ metadata: metadata,
277
+ environment_config: environment,
278
+ benchmark_config: config,
279
+ benchmark_result: results
280
+ )
281
+
282
+ results_file = File.join(result_dir, 'results.yaml')
283
+ results_model.to_file(results_file)
284
+
285
+ say '✅ Local benchmark completed successfully!', :green
286
+ say "Results saved to: #{result_dir}", :cyan
287
+ say "Generate site: serialbench benchmark build-site #{result_dir}", :white
288
+ rescue StandardError => e
289
+ say "❌ Local benchmark failed: #{e.message}", :red
290
+ say "Details: #{e.backtrace.first(3).join("\n")}", :white if options[:verbose]
291
+ raise e
292
+ end
293
+
294
+ def execute_docker_benchmark(environment, config, benchmark_config_path)
295
+ say '🐳 Executing Docker benchmark', :green
296
+
297
+ require_relative '../runners/docker_runner'
298
+
299
+ environment_config_path = "config/environments/#{environment.name}.yml"
300
+ runner = Runners::DockerRunner.new(environment, environment_config_path)
301
+
302
+ # Create results directory
303
+ result_dir = "results/runs/#{environment.name}-results"
304
+ FileUtils.mkdir_p(result_dir)
305
+
306
+ # Run benchmark
307
+ runner.run_benchmark(config, benchmark_config_path, result_dir)
308
+
309
+ say '✅ Docker benchmark completed successfully!', :green
310
+ say "Results saved to: #{result_dir}", :cyan
311
+ say "Generate site: serialbench benchmark build-site #{result_dir}", :white
312
+ rescue StandardError => e
313
+ say "❌ Docker benchmark failed: #{e.message}", :red
314
+ say "Details: #{e.backtrace.first(3).join("\n")}", :white if options[:verbose]
315
+ raise e
316
+ end
317
+
245
318
  def show_execute_usage_and_exit
246
319
  say '❌ Error: Environment and config file arguments are required.', :red
247
320
  say ''
@@ -350,7 +423,7 @@ module Serialbench
350
423
  # raise e
351
424
  # end
352
425
 
353
- def execute_asdf_benchmark(environment, config, benchmark_config_path)
426
+ def execute_asdf_benchmark(environment, _config, benchmark_config_path)
354
427
  say '🔧 Executing ASDF benchmark', :green
355
428
 
356
429
  # Use the ASDF runner to execute the benchmark
@@ -169,9 +169,9 @@ module Serialbench
169
169
  when 'asdf'
170
170
  require_relative '../runners/asdf_runner'
171
171
  Runners::AsdfRunner.new(environment_config, environment_config_path)
172
- # when 'local'
173
- # require_relative '../runners/local_runner'
174
- # Runners::LocalRunner.new(environment_config, environment_config_path)
172
+ when 'local'
173
+ require_relative '../runners/local_runner'
174
+ Runners::LocalRunner.new(environment_config, environment_config_path)
175
175
  else
176
176
  raise "Unknown environment type: #{environment_config.kind}"
177
177
  end
@@ -20,7 +20,7 @@ module Serialbench
20
20
  serialbench resultset create cross-platform-test results/sets/cross-platform-test
21
21
  DESC
22
22
  def create(resultset_name, resultset_path)
23
- store = Serialbench::Models::ResultStore.default
23
+ Serialbench::Models::ResultStore.default
24
24
 
25
25
  # Check if resultset already exists
26
26
  definition_path = File.join(resultset_path, 'resultset.yaml')
@@ -49,36 +49,83 @@ module Serialbench
49
49
  exit 1
50
50
  end
51
51
 
52
- desc 'add-result RESULT_PATH RESULTSET_PATH', 'Add a run to a resultset'
52
+ desc 'add-result RESULTSET_PATH RESULT_PATH...', 'Add one or more runs to a resultset'
53
53
  long_desc <<~DESC
54
- Add an existing benchmark run to a resultset.
54
+ Add one or more benchmark runs to a resultset.
55
55
 
56
- RESULT_PATH should be the path to a run result directory
57
- RESULTSET_PATH must be specified explicitly
56
+ RESULTSET_PATH is the path to the resultset directory
57
+ RESULT_PATH... accepts multiple result paths (supports shell expansion)
58
58
 
59
59
  Examples:
60
- serialbench resultset add-result results/sets/performance-comparison results/runs/my-run-local-macos-arm64-ruby-3.3.8
61
- serialbench resultset add-result results/sets/cross-platform-test results/runs/my-docker-run
60
+ # Add single result
61
+ serialbench resultset add-result results/sets/weekly results/runs/my-run
62
+
63
+ # Add multiple results explicitly
64
+ serialbench resultset add-result results/sets/weekly results/runs/run1 results/runs/run2
65
+
66
+ # Add multiple results with shell expansion
67
+ serialbench resultset add-result results/sets/weekly artifacts/benchmark-results-*/
68
+ serialbench resultset add-result results/sets/weekly results/runs/*
62
69
  DESC
63
- def add_result(resultset_path, result_path)
70
+ def add_result(resultset_path, *result_paths)
71
+ if result_paths.empty?
72
+ say '❌ Error: At least one result path must be provided', :red
73
+ exit 1
74
+ end
75
+
64
76
  resultset = Serialbench::Models::ResultSet.load(resultset_path)
65
77
 
66
- # Validate that the run location exists
67
- unless Dir.exist?(result_path)
68
- say "Result directory not found: #{result_path}", :red
69
- return
78
+ say "📦 Adding #{result_paths.size} result(s) to resultset", :cyan
79
+ say "ResultSet: #{resultset_path}", :white
80
+ say ''
81
+
82
+ added_count = 0
83
+ failed_count = 0
84
+ skipped_count = 0
85
+
86
+ result_paths.each_with_index do |result_path, index|
87
+ say "#{index + 1}/#{result_paths.size} Processing: #{result_path}", :cyan
88
+
89
+ # Find results.yaml in the path or subdirectories
90
+ results_file = if File.exist?(File.join(result_path, 'results.yaml'))
91
+ File.join(result_path, 'results.yaml')
92
+ else
93
+ Dir.glob(File.join(result_path, '**/results.yaml')).first
94
+ end
95
+
96
+ unless results_file
97
+ say ' ⚠️ No results.yaml found - skipping', :yellow
98
+ skipped_count += 1
99
+ next
100
+ end
101
+
102
+ result_dir = File.dirname(results_file)
103
+
104
+ begin
105
+ resultset.add_result(result_dir)
106
+ say ' ✅ Added successfully', :green
107
+ added_count += 1
108
+ rescue StandardError => e
109
+ say " ❌ Failed: #{e.message}", :red
110
+ failed_count += 1
111
+ end
112
+ say ''
70
113
  end
71
114
 
72
- # Add run to resultset
73
- resultset.add_result(result_path)
74
115
  resultset.save(resultset_path)
75
116
 
76
- say ' Added run to resultset', :green
77
- say "Path: #{result_path}", :cyan
78
- say "ResultSet: #{resultset_path}", :cyan
79
- say "Total runs in set: #{resultset.results.count}", :white
117
+ say '=' * 60, :cyan
118
+ say 'Summary:', :green
119
+ say " Total processed: #{result_paths.size}", :white
120
+ say " Successfully added: #{added_count}", :green
121
+ say " ❌ Failed: #{failed_count}", :red if failed_count > 0
122
+ say " ⚠️ Skipped: #{skipped_count}", :yellow if skipped_count > 0
123
+ say " 📊 Total results in set: #{resultset.results.count}", :cyan
124
+ say '=' * 60, :cyan
125
+
126
+ exit 1 if failed_count > 0 && added_count == 0
80
127
  rescue StandardError => e
81
- say "Error adding run to resultset: #{e.message}", :red
128
+ say "Error: #{e.message}", :red
82
129
  exit 1
83
130
  end
84
131
 
@@ -93,8 +140,8 @@ module Serialbench
93
140
  serialbench resultset remove-result results/sets/performance-comparison results/runs/my-run-local-macos-arm64-ruby-3.3.8
94
141
  serialbench resultset remove-result results/sets/cross-platform-test results/runs/my-docker-run
95
142
  DESC
96
- def remove_result(resultset_path, result_path)
97
- store = Serialbench::Models::ResultStore.default
143
+ def remove_result(resultset_path, _result_path)
144
+ Serialbench::Models::ResultStore.default
98
145
 
99
146
  # Find the resultset
100
147
  resultset = Serialbench::Models::ResultSet.load(resultset_path)
@@ -136,8 +183,7 @@ module Serialbench
136
183
  serialbench resultset build-site results/sets/performance-comparison
137
184
  serialbench resultset build-site results/sets/cross-platform-test output/
138
185
  DESC
139
- option :output_dir, type: :string, default: '_site', desc: 'Output directory for generated site'
140
- def build_site(resultset_path)
186
+ def build_site(resultset_path, output_dir = '_site')
141
187
  unless Dir.exist?(resultset_path)
142
188
  say "ResultSet directory not found: #{resultset_path}", :red
143
189
  say "Please create a resultset first using 'serialbench resultset create'", :white
@@ -156,17 +202,57 @@ module Serialbench
156
202
  say "Runs in set: #{resultset.results.size}", :cyan
157
203
 
158
204
  # Use the unified site generator for resultsets
159
- Serialbench::SiteGenerator.generate_for_resultset(resultset, options[:output_dir])
205
+ Serialbench::SiteGenerator.generate_for_resultset(resultset, output_dir)
160
206
 
161
207
  say '✅ HTML site generated successfully!', :green
162
- say "Site location: #{options[:output_dir]}", :cyan
163
- say "Open: #{File.join(options[:output_dir], 'index.html')}", :white
208
+ say "Site location: #{output_dir}", :cyan
209
+ say "Open: #{File.join(output_dir, 'index.html')}", :white
164
210
  rescue StandardError => e
165
211
  say "Error generating site: #{e.message}", :red
166
212
  say "Details: #{e.backtrace.first(3).join("\n")}", :red if options[:verbose]
167
213
  exit 1
168
214
  end
169
215
 
216
+ desc 'export-data RESULTSET_PATH OUTPUT_JSON', 'Export a resultset as the site dashboard JSON payload'
217
+ long_desc <<~DESC
218
+ Write the aggregated dashboard payload (combined_results, environments,
219
+ metadata) for a resultset as JSON. This is the data contract consumed
220
+ by the Astro site in site/.
221
+
222
+ Optionally also export the raw per-environment YAML files and the
223
+ complete resultset YAML for download links.
224
+
225
+ Examples:
226
+ serialbench resultset export-data results/sets/weekly site/src/data/sample.json --raw-data-dir site/public/data
227
+ DESC
228
+ method_option 'raw-data-dir', type: :string,
229
+ desc: 'Also write per-environment YAML and resultset.yaml to this directory'
230
+ def export_data(resultset_path, output_json)
231
+ resultset = Serialbench::Models::ResultSet.load(resultset_path)
232
+
233
+ if resultset.results.empty?
234
+ say "ResultSet '#{resultset_path}' contains no runs", :yellow
235
+ say "Use 'serialbench resultset add-result' to add runs first", :white
236
+ exit 1
237
+ end
238
+
239
+ payload = Serialbench::SiteGenerator.resultset_payload(resultset)
240
+ output_dir = File.dirname(File.expand_path(output_json))
241
+ FileUtils.mkdir_p(output_dir)
242
+ File.write(output_json, JSON.pretty_generate(payload))
243
+
244
+ say "✅ Dashboard payload exported: #{output_json}", :green
245
+ say "Environments: #{payload['environments'].size}", :cyan
246
+
247
+ if options['raw-data-dir']
248
+ Serialbench::SiteGenerator.export_raw_data(resultset, options['raw-data-dir'])
249
+ say "📦 Raw YAML exported to: #{options['raw-data-dir']}", :cyan
250
+ end
251
+ rescue StandardError => e
252
+ say "Error exporting data: #{e.message}", :red
253
+ exit 1
254
+ end
255
+
170
256
  desc 'list', 'List all available resultsets'
171
257
  long_desc <<~DESC
172
258
  List all resultsets in the results/sets/ directory.