ace-test-runner 0.19.1 → 0.25.7

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.
@@ -3,8 +3,8 @@
3
3
  module Ace
4
4
  module TestRunner
5
5
  module Models
6
- # Represents a group of tests to be executed together
7
- class TestGroup
6
+ # Represents a target of tests to be executed together
7
+ class TestTarget
8
8
  attr_reader :name, :patterns, :files, :options
9
9
 
10
10
  def initialize(name:, patterns: [], files: [], options: {})
@@ -14,7 +14,7 @@ module Ace
14
14
  @options = options
15
15
  end
16
16
 
17
- # Find all test files matching this group's patterns
17
+ # Find all test files matching this target's patterns
18
18
  def find_files
19
19
  return @files unless @files.empty?
20
20
 
@@ -22,7 +22,7 @@ module Ace
22
22
  detector.find_test_files
23
23
  end
24
24
 
25
- # Execute this group's tests
25
+ # Execute this target's tests
26
26
  def execute(executor, formatter_options = {})
27
27
  test_files = find_files
28
28
  return empty_result if test_files.empty?
@@ -31,7 +31,7 @@ module Ace
31
31
  executor.execute_tests(test_files, options)
32
32
  end
33
33
 
34
- # Check if a file belongs to this group
34
+ # Check if a file belongs to this target
35
35
  def includes_file?(file_path)
36
36
  return true if @files.include?(file_path)
37
37
 
@@ -49,11 +49,11 @@ module Ace
49
49
  }
50
50
  end
51
51
 
52
- # Load groups from configuration
52
+ # Load targets from configuration
53
53
  def self.from_config(config)
54
- groups = config.fetch("groups", default_groups)
54
+ targets = config.fetch("targets", default_targets)
55
55
 
56
- groups.map do |name, definition|
56
+ targets.map do |name, definition|
57
57
  new(
58
58
  name: name,
59
59
  patterns: definition["patterns"] || [],
@@ -63,23 +63,19 @@ module Ace
63
63
  end
64
64
  end
65
65
 
66
- # Default test groups for common Ruby project structures
67
- def self.default_groups
66
+ # Default test targets for common Ruby project structures
67
+ def self.default_targets
68
68
  {
69
- "unit" => {
70
- "patterns" => ["test/unit/**/*_test.rb", "test/*_test.rb"],
69
+ "fast" => {
70
+ "patterns" => ["test/fast/**/*_test.rb", "test/*_test.rb", "test/unit/**/*_test.rb"],
71
71
  "options" => {}
72
72
  },
73
- "integration" => {
74
- "patterns" => ["test/integration/**/*_test.rb"],
73
+ "feat" => {
74
+ "patterns" => ["test/feat/**/*_test.rb", "test/edge/**/*_test.rb"],
75
75
  "options" => {}
76
76
  },
77
- "system" => {
78
- "patterns" => ["test/system/**/*_test.rb"],
79
- "options" => {"timeout" => 60}
80
- },
81
77
  "all" => {
82
- "patterns" => ["test/**/*_test.rb"],
78
+ "patterns" => ["test/fast/**/*_test.rb", "test/*_test.rb", "test/feat/**/*_test.rb", "test/edge/**/*_test.rb"],
83
79
  "options" => {}
84
80
  }
85
81
  }
@@ -90,7 +86,7 @@ module Ace
90
86
  def empty_result
91
87
  {
92
88
  stdout: "",
93
- stderr: "No test files found for group '#{@name}'",
89
+ stderr: "No test files found for target '#{@name}'",
94
90
  status: OpenStruct.new(success?: true, exitstatus: 0),
95
91
  command: "",
96
92
  start_time: Time.now,
@@ -10,9 +10,9 @@ module Ace
10
10
  # - Direct file paths (./path/file.rb, ../path/file.rb, /abs/path/file.rb)
11
11
  # - Package-prefixed file paths (ace-bundle/test/file.rb)
12
12
  # - Package names (ace-bundle)
13
- # - Test targets (atoms, molecules, unit, etc.)
13
+ # - Test targets (atoms, molecules, fast, feat, etc.)
14
14
  class CliArgumentParser
15
- KNOWN_TARGETS = %w[atoms molecules organisms models unit integration system all quick].freeze
15
+ KNOWN_TARGETS = %w[atoms molecules organisms models fast feat all quick].freeze
16
16
 
17
17
  attr_reader :package_dir, :target, :test_files
18
18
 
@@ -145,7 +145,7 @@ module Ace
145
145
  def normalize_config(config)
146
146
  # Ensure all sections exist as hashes (defaults already merged in load)
147
147
  config[:patterns] ||= {}
148
- config[:groups] ||= {}
148
+ config[:targets] ||= {}
149
149
  config[:defaults] ||= {}
150
150
  config[:failure_limits] ||= {}
151
151
  config[:execution] ||= {}
@@ -4,24 +4,30 @@ module Ace
4
4
  module TestRunner
5
5
  module Molecules
6
6
  class PatternResolver
7
+ INTERNAL_PATTERN_LABELS = {
8
+ "feat_tests" => "feat"
9
+ }.freeze
10
+
7
11
  attr_reader :using_catch_all
8
12
 
9
13
  def initialize(config)
10
14
  @config = config
11
15
  @patterns = normalize_keys(config.patterns || {})
12
- @groups = normalize_keys(config.groups || {})
16
+ @targets = normalize_keys(config.targets || {})
13
17
  @using_catch_all = false
14
18
  end
15
19
 
16
20
  def resolve_target(target)
17
- return resolve_all_files if target.nil? || target == "all"
21
+ return resolve_named_target("fast") if target.nil?
22
+ return resolve_all_files if target == "all"
18
23
  return [target] if File.exist?(target)
19
24
 
20
25
  # Normalize target to string for consistent lookup
21
26
  target_key = target.to_s
27
+ raise_unsupported_target_error(target_key) if %w[e2e all-with-e2e system].include?(target_key)
22
28
 
23
- if @groups.key?(target_key)
24
- resolve_group(target_key)
29
+ if @targets.key?(target_key)
30
+ resolve_named_target(target_key)
25
31
  elsif @patterns.key?(target_key)
26
32
  expand_pattern(@patterns[target_key])
27
33
  elsif looks_like_file_path?(target)
@@ -36,23 +42,24 @@ module Ace
36
42
  targets.flat_map { |target| resolve_target(target) }.uniq
37
43
  end
38
44
 
39
- def resolve_group_sequential(group_name)
40
- group_key = group_name.to_s
41
- group_members = @groups[group_key]
42
- return [] unless group_members
45
+ def resolve_target_sequential(target_name, seen = [])
46
+ target_key = target_name.to_s
47
+ if seen.include?(target_key)
48
+ raise ArgumentError, "Cyclic test target definition detected: #{(seen + [target_key]).join(' -> ')}"
49
+ end
50
+
51
+ target_members = @targets[target_key]
52
+ return [] unless target_members
43
53
 
44
- group_members.flat_map do |member|
54
+ target_members.flat_map do |member|
45
55
  member_key = member.to_s
46
56
 
47
- if @groups.key?(member_key)
48
- # Recursively expand nested groups
49
- resolve_group_sequential(member_key)
57
+ if @targets.key?(member_key)
58
+ resolve_target_sequential(member_key, seen + [target_key])
50
59
  elsif @patterns.key?(member_key)
51
- # Pattern found - return as a group
52
60
  files = expand_pattern(@patterns[member_key])
53
- files.empty? ? [] : [{name: member_key, files: files}]
61
+ files.empty? ? [] : [{name: display_name_for(member_key), files: files}]
54
62
  else
55
- # Direct pattern - expand and wrap
56
63
  files = expand_pattern(member)
57
64
  files.empty? ? [] : [{name: "other", files: files}]
58
65
  end
@@ -60,19 +67,35 @@ module Ace
60
67
  end
61
68
 
62
69
  def available_targets
63
- (@groups.keys + @patterns.keys).map(&:to_s).sort
70
+ (@targets.keys + @patterns.keys)
71
+ .map(&:to_s)
72
+ .reject { |name| INTERNAL_PATTERN_LABELS.key?(name) }
73
+ .sort
64
74
  end
65
75
 
66
76
  def classify_file(file_path)
67
77
  @patterns.each do |name, pattern|
68
- # Use File::FNM_PATHNAME to handle ** correctly
69
- return name.to_s if File.fnmatch?(pattern, file_path, File::FNM_PATHNAME)
78
+ if File.fnmatch?(pattern, file_path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
79
+ return display_name_for(name)
80
+ end
70
81
  end
71
82
  "other"
72
83
  end
73
84
 
74
85
  private
75
86
 
87
+ def display_name_for(name)
88
+ INTERNAL_PATTERN_LABELS.fetch(name.to_s, name.to_s)
89
+ end
90
+
91
+ def raise_unsupported_target_error(target)
92
+ if target == "e2e"
93
+ raise ArgumentError, "Unsupported target: e2e. Use `ace-test-e2e <package>`."
94
+ end
95
+
96
+ raise ArgumentError, "Unsupported target: #{target}"
97
+ end
98
+
76
99
  def looks_like_file_path?(target)
77
100
  target.include?("/") || target.end_with?(".rb")
78
101
  end
@@ -81,21 +104,23 @@ module Ace
81
104
  hash.transform_keys(&:to_s)
82
105
  end
83
106
 
84
- def resolve_group(group_name)
85
- # Normalize to string
86
- group_key = group_name.to_s
87
- group_members = @groups[group_key]
88
- return [] unless group_members
107
+ def resolve_named_target(target_name, seen = [])
108
+ target_key = target_name.to_s
109
+ if seen.include?(target_key)
110
+ raise ArgumentError, "Cyclic test target definition detected: #{(seen + [target_key]).join(' -> ')}"
111
+ end
112
+
113
+ target_members = @targets[target_key]
114
+ return [] unless target_members
89
115
 
90
- group_members.flat_map do |member|
116
+ target_members.flat_map do |member|
91
117
  member_key = member.to_s
92
118
 
93
- if @groups.key?(member_key)
94
- resolve_group(member_key) # Recursive expansion
119
+ if @targets.key?(member_key)
120
+ resolve_named_target(member_key, seen + [target_key])
95
121
  elsif @patterns.key?(member_key)
96
122
  expand_pattern(@patterns[member_key])
97
123
  else
98
- # Direct pattern or file
99
124
  expand_pattern(member)
100
125
  end
101
126
  end.uniq
@@ -106,19 +131,15 @@ module Ace
106
131
  end
107
132
 
108
133
  def resolve_all_files
109
- # Always start by finding ALL test files that exist
134
+ # Catch-all default selection should not silently widen back into
135
+ # deterministic E2E tests when the configured "all" target excludes them.
110
136
  all_test_files = expand_pattern("test/**/*_test.rb")
137
+ catch_all_files = all_test_files - expand_pattern("test/e2e/**/*_test.rb")
111
138
  @using_catch_all = false
112
139
 
113
- # First check if "all" group is defined
114
- if @groups.key?("all")
115
- pattern_files = resolve_group("all")
116
- # If patterns miss any files, use the complete scan
117
- if pattern_files.size < all_test_files.size
118
- @using_catch_all = true
119
- return all_test_files
120
- end
121
- return pattern_files
140
+ # First check if "all" target is defined
141
+ if @targets.key?("all")
142
+ return resolve_named_target("all")
122
143
  end
123
144
 
124
145
  # If no "all" group, try all defined patterns
@@ -131,14 +152,14 @@ module Ace
131
152
  # If patterns miss any files, use the complete scan
132
153
  if pattern_files.size < all_test_files.size
133
154
  @using_catch_all = true
134
- return all_test_files
155
+ return catch_all_files
135
156
  end
136
157
  return pattern_files
137
158
  end
138
159
 
139
160
  # No configuration - using catch-all pattern
140
161
  @using_catch_all = true
141
- all_test_files
162
+ catch_all_files
142
163
  end
143
164
  end
144
165
  end
@@ -65,9 +65,9 @@ module Ace
65
65
  return :subprocess if options[:subprocess] || @force_mode == :subprocess
66
66
  return :direct if options[:direct] || @force_mode == :direct
67
67
 
68
- # Check group_isolation config for sequential group execution
68
+ # Check target_isolation config for sequential target execution
69
69
  # true = subprocess for better isolation
70
- if options[:group_isolation] == true
70
+ if options[:target_isolation] == true
71
71
  return :subprocess
72
72
  end
73
73
 
@@ -3,28 +3,28 @@
3
3
  module Ace
4
4
  module TestRunner
5
5
  module Organisms
6
- # Executes test groups sequentially with visual separation and fail-fast support
7
- class SequentialGroupExecutor
6
+ # Executes test targets sequentially with visual separation and fail-fast support
7
+ class SequentialTargetExecutor
8
8
  def initialize(test_executor:, result_parser:, formatter: nil)
9
9
  @test_executor = test_executor
10
10
  @result_parser = result_parser
11
11
  @formatter = formatter
12
12
  end
13
13
 
14
- def execute_groups(groups, options = {})
15
- fail_fast = options[:fail_fast] || options[:group_fail_fast]
14
+ def execute_targets(targets, options = {})
15
+ fail_fast = options[:fail_fast] || options[:target_fail_fast]
16
16
  all_results = []
17
17
  all_files = []
18
- group_results = []
18
+ target_results = []
19
19
 
20
- groups.each do |group|
21
- group_name = group[:name]
22
- files = group[:files]
20
+ targets.each do |target|
21
+ target_name = target[:name]
22
+ files = target[:files]
23
23
  next if files.empty?
24
24
 
25
- # Notify formatter of group start
26
- if @formatter&.respond_to?(:on_group_start)
27
- @formatter.on_group_start(group_name, files.size)
25
+ # Notify formatter of target start
26
+ if @formatter&.respond_to?(:on_target_start)
27
+ @formatter.on_target_start(target_name, files.size)
28
28
  end
29
29
 
30
30
  start_time = Time.now
@@ -45,20 +45,20 @@ module Ace
45
45
  duration = Time.now - start_time
46
46
  success = result[:success]
47
47
 
48
- # Store group result
49
- group_result = {
50
- name: group_name,
48
+ # Store target result
49
+ target_result = {
50
+ name: target_name,
51
51
  success: success,
52
52
  duration: duration,
53
53
  parsed: parsed,
54
54
  files: files
55
55
  }
56
- group_results << group_result
56
+ target_results << target_result
57
57
 
58
- # Notify formatter of group completion
59
- if @formatter&.respond_to?(:on_group_complete)
60
- @formatter.on_group_complete(
61
- group_name,
58
+ # Notify formatter of target completion
59
+ if @formatter&.respond_to?(:on_target_complete)
60
+ @formatter.on_target_complete(
61
+ target_name,
62
62
  success,
63
63
  duration,
64
64
  parsed[:summary]
@@ -69,15 +69,14 @@ module Ace
69
69
  all_results << result
70
70
  all_files.concat(files)
71
71
 
72
- # Stop if group failed and fail_fast is enabled
72
+ # Stop if target failed and fail_fast is enabled
73
73
  if fail_fast && !success
74
- # Return early with aggregated results so far
75
- return build_aggregated_result(group_results, all_files, all_results, stopped: group_name)
74
+ return build_aggregated_result(target_results, all_files, all_results, stopped: target_name)
76
75
  end
77
76
  end
78
77
 
79
- # All groups completed successfully (or fail_fast not enabled)
80
- build_aggregated_result(group_results, all_files, all_results)
78
+ # All targets completed successfully (or fail_fast not enabled)
79
+ build_aggregated_result(target_results, all_files, all_results)
81
80
  end
82
81
 
83
82
  private
@@ -123,7 +122,7 @@ module Ace
123
122
  aggregated
124
123
  end
125
124
 
126
- def build_aggregated_result(group_results, all_files, all_results, stopped: nil)
125
+ def build_aggregated_result(target_results, all_files, all_results, stopped: nil)
127
126
  # Aggregate all parsed results
128
127
  total_summary = {
129
128
  runs: 0,
@@ -138,8 +137,8 @@ module Ace
138
137
  total_duration = 0.0
139
138
  all_test_times = []
140
139
 
141
- group_results.each do |gr|
142
- parsed = gr[:parsed]
140
+ target_results.each do |tr|
141
+ parsed = tr[:parsed]
143
142
  total_summary[:runs] += parsed[:summary][:runs]
144
143
  total_summary[:assertions] += parsed[:summary][:assertions]
145
144
  total_summary[:failures] += parsed[:summary][:failures]
@@ -149,7 +148,7 @@ module Ace
149
148
 
150
149
  all_failures.concat(parsed[:failures] || [])
151
150
  all_deprecations.concat(parsed[:deprecations] || [])
152
- total_duration += gr[:duration]
151
+ total_duration += tr[:duration]
153
152
  all_test_times.concat(parsed[:test_times] || [])
154
153
  end
155
154
 
@@ -176,7 +175,7 @@ module Ace
176
175
  duration: total_duration,
177
176
  test_times: all_test_times
178
177
  },
179
- stopped_at_group: stopped
178
+ stopped_at_target: stopped
180
179
  }
181
180
  end
182
181
  end
@@ -4,7 +4,7 @@ require_relative "../formatters/base_formatter"
4
4
  require_relative "../molecules/config_loader"
5
5
  require_relative "../molecules/pattern_resolver"
6
6
  require_relative "../atoms/report_directory_resolver"
7
- require_relative "sequential_group_executor"
7
+ require_relative "sequential_target_executor"
8
8
 
9
9
  module Ace
10
10
  module TestRunner
@@ -63,11 +63,11 @@ module Ace
63
63
  puts "Running tests in #{File.basename(@package_dir)}..."
64
64
  end
65
65
 
66
- # Check if sequential group execution should be used
66
+ # Check if sequential target execution should be used
67
67
  if should_execute_sequentially?
68
- # Use default "all" group if no target specified in grouped mode
69
- @configuration.target ||= "all"
70
- return run_sequential_groups(start_time)
68
+ # Use default "fast" target if no target specified in by-target mode
69
+ @configuration.target ||= "fast"
70
+ return run_sequential_targets(start_time)
71
71
  end
72
72
 
73
73
  # Find test files
@@ -116,7 +116,7 @@ module Ace
116
116
  # Each file was executed separately, parse and sum them all
117
117
  aggregate_individual_results(execution_result[:stdout])
118
118
  else
119
- # Single command execution (grouped)
119
+ # Single command execution (by-target)
120
120
  @result_parser.parse_output(execution_result[:stdout])
121
121
  end
122
122
 
@@ -199,21 +199,19 @@ module Ace
199
199
  if @configuration.failure_limits
200
200
  formatter_options[:max_failures_to_display] = @configuration.failure_limits[:max_display]
201
201
  end
202
- # Disable group headers in on_test_complete to avoid duplicates with on_group_start/complete
202
+ # Disable target headers in on_test_complete to avoid duplicates with on_target_start/complete
203
203
  formatter_options[:show_groups] = false
204
204
  @formatter = @configuration.formatter_class.new(formatter_options)
205
205
  end
206
206
 
207
- def run_sequential_groups(start_time)
208
- # Resolve groups sequentially
209
- groups = @pattern_resolver.resolve_group_sequential(@configuration.target)
207
+ def run_sequential_targets(start_time)
208
+ targets = @pattern_resolver.resolve_target_sequential(@configuration.target)
210
209
 
211
- if groups.empty?
210
+ if targets.empty?
212
211
  return handle_no_tests
213
212
  end
214
213
 
215
- # Count total files
216
- all_files = groups.flat_map { |g| g[:files] }
214
+ all_files = targets.flat_map { |t| t[:files] }
217
215
 
218
216
  resolve_and_set_report_dir_context(all_files)
219
217
 
@@ -226,15 +224,13 @@ module Ace
226
224
  @formatter.on_start(all_files.size)
227
225
  end
228
226
 
229
- # Create sequential executor
230
- executor = SequentialGroupExecutor.new(
227
+ executor = SequentialTargetExecutor.new(
231
228
  test_executor: @test_executor,
232
229
  result_parser: @result_parser,
233
230
  formatter: @formatter
234
231
  )
235
232
 
236
- # Execute groups sequentially
237
- execution_result = executor.execute_groups(groups, sequential_options) do |event|
233
+ execution_result = executor.execute_targets(targets, sequential_options) do |event|
238
234
  case event[:type]
239
235
  when :stdout
240
236
  @formatter.on_test_stdout(event[:content]) if @formatter.respond_to?(:on_test_stdout)
@@ -297,8 +293,8 @@ module Ace
297
293
  end
298
294
 
299
295
  # Show stopped message if execution was stopped
300
- if execution_result[:stopped_at_group]
301
- puts "\nSTOPPED: Group '#{execution_result[:stopped_at_group]}' failed (--fail-fast enabled)"
296
+ if execution_result[:stopped_at_target]
297
+ puts "\nSTOPPED: Target '#{execution_result[:stopped_at_target]}' failed (--fail-fast enabled)"
302
298
  end
303
299
 
304
300
  # Return exit code
@@ -310,10 +306,9 @@ module Ace
310
306
  # This ensures that commands like `ace-test test/atoms/foo_test.rb` run only that file
311
307
  return false if @configuration.files && !@configuration.files.empty?
312
308
 
313
- # Only use sequential groups if execution_mode is "grouped"
314
- return false unless @configuration.execution_mode == "grouped"
309
+ return false unless @configuration.execution_mode == "by-target"
315
310
 
316
- # Explicit --run-in-single-batch bypasses grouped execution
311
+ # Explicit --run-in-single-batch bypasses by-target execution
317
312
  return false if @configuration.run_in_single_batch
318
313
 
319
314
  # When profiling without a specific target, run as single batch for accurate timing
@@ -322,14 +317,14 @@ module Ace
322
317
  return false
323
318
  end
324
319
 
325
- # If no target specified, default to "all" group for grouped mode
326
- target = @configuration.target || "all"
320
+ # If no target specified, default to "fast" target for by-target mode
321
+ target = @configuration.target || "fast"
327
322
 
328
323
  target_str = target.to_s
329
324
  target_sym = target.to_sym
330
325
 
331
326
  # Check both string and symbol keys for compatibility
332
- @configuration.groups&.key?(target_str) || @configuration.groups&.key?(target_sym)
327
+ @configuration.targets&.key?(target_str) || @configuration.targets&.key?(target_sym)
333
328
  end
334
329
 
335
330
  def sequential_options
@@ -338,8 +333,8 @@ module Ace
338
333
  verbose: @configuration.verbose,
339
334
  per_file: @configuration.per_file,
340
335
  profile: @configuration.profile,
341
- group_fail_fast: @configuration.execution&.[](:group_fail_fast),
342
- group_isolation: @configuration.group_isolation
336
+ target_fail_fast: @configuration.execution&.[](:target_fail_fast),
337
+ target_isolation: @configuration.target_isolation
343
338
  }
344
339
  end
345
340
 
@@ -361,7 +356,7 @@ module Ace
361
356
  filter: options[:filter],
362
357
  fix_deprecations: options[:fix_deprecations],
363
358
  patterns: config_with_options.patterns,
364
- groups: config_with_options.groups,
359
+ targets: config_with_options.targets,
365
360
  target: options[:target],
366
361
  config_path: options[:config_path],
367
362
  timeout: options[:timeout],
@@ -413,7 +408,7 @@ module Ace
413
408
  exit 1
414
409
  end
415
410
 
416
- # If using catch-all pattern, reinitialize formatter without groups
411
+ # If using catch-all pattern, reinitialize formatter without target headers
417
412
  if @pattern_resolver.using_catch_all
418
413
  formatter_options = @configuration.to_h.merge(show_groups: false)
419
414
  if @configuration.failure_limits
@@ -454,11 +449,11 @@ module Ace
454
449
  verbose: @configuration.verbose,
455
450
  per_file: @configuration.per_file, # Allow per-file execution if needed for debugging
456
451
  profile: @configuration.profile, # Add profile option for verbose timing
457
- group_isolation: @configuration.group_isolation # Pass through for execution mode selection
452
+ target_isolation: @configuration.target_isolation # Pass through for execution mode selection
458
453
  }
459
454
 
460
455
  # Always use execute_with_progress for consistent interface
461
- # The method internally decides whether to run per-file or grouped
456
+ # The method internally decides whether to run per-file or by-target
462
457
  @test_executor.execute_with_progress(test_files, options) do |event|
463
458
  case event[:type]
464
459
  when :stdout