Class: SplitTestRb::CLI
- Inherits:
-
Object
- Object
- SplitTestRb::CLI
- Defined in:
- lib/split_test_rb.rb
Overview
Command-line interface
Constant Summary collapse
- DEFAULT_OPTIONS =
Default option values for CLI
{ node_index: 0, total_nodes: 1, debug: false, test_dir: 'spec', test_pattern: '**/*_spec.rb', split_by_example_threshold: nil }.freeze
Class Method Summary collapse
-
.add_missing_files_with_default_timing(timings, all_test_files) ⇒ Object
Adds test files missing from JSON results with default timing (1.0s).
-
.apply_example_splitting(file_timings, json_files, threshold) ⇒ Object
Splits heavy files (>= threshold) into individual examples.
-
.build_option_parser(options) ⇒ Object
Builds and configures the OptionParser instance.
-
.define_node_options(opts, options) ⇒ Object
Defines node distribution related CLI options.
-
.define_options(opts, options) ⇒ Object
Defines all CLI options on the given OptionParser.
-
.define_test_options(opts, options) ⇒ Object
Defines test configuration and utility CLI options.
- .exit_if_no_tests(timings) ⇒ Object
- .find_all_spec_files(test_dir = 'spec', test_pattern = '**/*_spec.rb') ⇒ Object
- .load_timings(options) ⇒ Object
- .load_timings_from_json(json_dir, options) ⇒ Object
- .output_node_files(nodes, node_index) ⇒ Object
-
.parse_options(argv) ⇒ Object
Parses command-line arguments and returns options hash.
- .run(argv) ⇒ Object
- .validate_options!(options) ⇒ Object
Class Method Details
.add_missing_files_with_default_timing(timings, all_test_files) ⇒ Object
Adds test files missing from JSON results with default timing (1.0s)
218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
# File 'lib/split_test_rb.rb', line 218 def self.add_missing_files_with_default_timing(timings, all_test_files) default_files = Set.new missing_files = all_test_files.keys - timings.keys return default_files if missing_files.empty? warn "Warning: Found #{missing_files.size} test files not in JSON, adding with default execution time" missing_files.each do |file| timings[file] = 1.0 default_files.add(file) end default_files end |
.apply_example_splitting(file_timings, json_files, threshold) ⇒ Object
Splits heavy files (>= threshold) into individual examples
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 |
# File 'lib/split_test_rb.rb', line 198 def self.apply_example_splitting(file_timings, json_files, threshold) heavy_files = file_timings.select { |_file, time| time >= threshold } return file_timings if heavy_files.empty? example_timings = JsonParser.parse_files_with_examples(json_files) # Start with light files (below threshold) timings = file_timings.reject { |file, _| heavy_files.key?(file) } # Add individual examples from heavy files heavy_files.each_key do |heavy_file| example_timings.each do |example_id, time| timings[example_id] = time if example_id.start_with?(heavy_file) end end timings end |
.build_option_parser(options) ⇒ Object
Builds and configures the OptionParser instance
263 264 265 266 267 268 |
# File 'lib/split_test_rb.rb', line 263 def self.build_option_parser() OptionParser.new do |opts| opts. = 'Usage: split-test-rb [options]' (opts, ) end end |
.define_node_options(opts, options) ⇒ Object
Defines node distribution related CLI options
277 278 279 280 281 |
# File 'lib/split_test_rb.rb', line 277 def self.(opts, ) opts.on('--node-index INDEX', Integer, 'Current node index (0-based)') { |v| [:node_index] = v } opts.on('--node-total TOTAL', Integer, 'Total number of nodes') { |v| [:total_nodes] = v } opts.on('--json-path PATH', 'Path to directory containing RSpec JSON reports') { |v| [:json_path] = v } end |
.define_options(opts, options) ⇒ Object
Defines all CLI options on the given OptionParser
271 272 273 274 |
# File 'lib/split_test_rb.rb', line 271 def self.(opts, ) (opts, ) (opts, ) end |
.define_test_options(opts, options) ⇒ Object
Defines test configuration and utility CLI options
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 |
# File 'lib/split_test_rb.rb', line 284 def self.(opts, ) opts.on('--test-dir DIR', 'Test directory (default: spec)') { |v| [:test_dir] = v } opts.on('--test-pattern PATTERN', 'Test file pattern (default: **/*_spec.rb)') { |v| [:test_pattern] = v } opts.on('--split-by-example-threshold SECONDS', Float, 'Split files with execution time >= threshold into individual examples') do |v| [:split_by_example_threshold] = v end opts.on('--debug', 'Show debug information') { [:debug] = true } opts.on('-h', '--help', 'Show this help message') do puts opts exit end opts.on('-v', '--version', 'Show version') do puts "split-test-rb #{VERSION}" exit end end |
.exit_if_no_tests(timings) ⇒ Object
233 234 235 236 237 238 |
# File 'lib/split_test_rb.rb', line 233 def self.exit_if_no_tests(timings) return unless timings.empty? warn 'Warning: No test files found' exit 0 end |
.find_all_spec_files(test_dir = 'spec', test_pattern = '**/*_spec.rb') ⇒ Object
302 303 304 305 306 307 308 309 310 311 |
# File 'lib/split_test_rb.rb', line 302 def self.find_all_spec_files(test_dir = 'spec', test_pattern = '**/*_spec.rb') # Find all test files in the specified directory with the given pattern glob_pattern = File.join(test_dir, test_pattern) test_files = Dir.glob(glob_pattern) # Normalize paths and assign equal execution time (1.0) to each file test_files.each_with_object({}) do |file, hash| normalized_path = JsonParser.normalize_path(file) hash[normalized_path] = 1.0 end end |
.load_timings(options) ⇒ Object
164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/split_test_rb.rb', line 164 def self.load_timings() json_dir = [:json_path] if File.directory?(json_dir) load_timings_from_json(json_dir, ) else warn "Warning: JSON directory not found: #{json_dir}, using all test files with equal execution time" timings = find_all_spec_files([:test_dir], [:test_pattern]) [timings, Set.new(timings.keys), []] end end |
.load_timings_from_json(json_dir, options) ⇒ Object
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
# File 'lib/split_test_rb.rb', line 176 def self.load_timings_from_json(json_dir, ) json_files = Dir.glob(File.join(json_dir, '**', '*.json')) file_timings = JsonParser.parse_files(json_files) all_test_files = find_all_spec_files([:test_dir], [:test_pattern]) # Filter out files from JSON cache that don't match the test pattern file_timings.select! { |file, _| all_test_files.key?(file) } default_files = add_missing_files_with_default_timing(file_timings, all_test_files) # Apply example-level splitting if threshold is set threshold = [:split_by_example_threshold] timings = if threshold apply_example_splitting(file_timings, json_files, threshold) else file_timings end [timings, default_files, json_files] end |
.output_node_files(nodes, node_index) ⇒ Object
240 241 242 243 |
# File 'lib/split_test_rb.rb', line 240 def self.output_node_files(nodes, node_index) node_files = nodes[node_index][:files] puts node_files.join("\n") end |
.parse_options(argv) ⇒ Object
Parses command-line arguments and returns options hash
256 257 258 259 260 |
# File 'lib/split_test_rb.rb', line 256 def self.(argv) = DEFAULT_OPTIONS.dup build_option_parser().parse!(argv) end |
.run(argv) ⇒ Object
144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/split_test_rb.rb', line 144 def self.run(argv) = (argv) () timings, default_files, json_files = load_timings() exit_if_no_tests(timings) nodes = Balancer.balance(timings, [:total_nodes]) DebugPrinter.print(nodes, timings, default_files, json_files) if [:debug] output_node_files(nodes, [:node_index]) end |
.validate_options!(options) ⇒ Object
157 158 159 160 161 162 |
# File 'lib/split_test_rb.rb', line 157 def self.() return if [:json_path] warn 'Error: --json-path is required' exit 1 end |