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.
- checksums.yaml +4 -4
- data/.ace-defaults/test/runner.yml +9 -9
- data/.ace-defaults/test-runner/config.yml +24 -20
- data/CHANGELOG.md +124 -0
- data/README.md +6 -4
- data/exe/ace-test-suite +10 -0
- data/lib/ace/test_runner/atoms/test_folder_detector.rb +4 -4
- data/lib/ace/test_runner/atoms/test_type_detector.rb +9 -5
- data/lib/ace/test_runner/cli/commands/test.rb +13 -9
- data/lib/ace/test_runner/formatters/progress_formatter.rb +25 -28
- data/lib/ace/test_runner/models/test_configuration.rb +26 -20
- data/lib/ace/test_runner/models/{test_group.rb → test_target.rb} +16 -20
- data/lib/ace/test_runner/molecules/cli_argument_parser.rb +2 -2
- data/lib/ace/test_runner/molecules/config_loader.rb +1 -1
- data/lib/ace/test_runner/molecules/pattern_resolver.rb +60 -39
- data/lib/ace/test_runner/molecules/smart_test_executor.rb +2 -2
- data/lib/ace/test_runner/organisms/{sequential_group_executor.rb → sequential_target_executor.rb} +28 -29
- data/lib/ace/test_runner/organisms/test_orchestrator.rb +26 -31
- data/lib/ace/test_runner/suite/process_monitor.rb +38 -6
- data/lib/ace/test_runner/suite/result_aggregator.rb +11 -1
- data/lib/ace/test_runner/version.rb +1 -1
- metadata +14 -14
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module Ace
|
|
4
4
|
module TestRunner
|
|
5
5
|
module Models
|
|
6
|
-
# Represents a
|
|
7
|
-
class
|
|
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
|
|
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
|
|
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
|
|
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
|
|
52
|
+
# Load targets from configuration
|
|
53
53
|
def self.from_config(config)
|
|
54
|
-
|
|
54
|
+
targets = config.fetch("targets", default_targets)
|
|
55
55
|
|
|
56
|
-
|
|
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
|
|
67
|
-
def self.
|
|
66
|
+
# Default test targets for common Ruby project structures
|
|
67
|
+
def self.default_targets
|
|
68
68
|
{
|
|
69
|
-
"
|
|
70
|
-
"patterns" => ["test/
|
|
69
|
+
"fast" => {
|
|
70
|
+
"patterns" => ["test/fast/**/*_test.rb", "test/*_test.rb", "test/unit/**/*_test.rb"],
|
|
71
71
|
"options" => {}
|
|
72
72
|
},
|
|
73
|
-
"
|
|
74
|
-
"patterns" => ["test/
|
|
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
|
|
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,
|
|
13
|
+
# - Test targets (atoms, molecules, fast, feat, etc.)
|
|
14
14
|
class CliArgumentParser
|
|
15
|
-
KNOWN_TARGETS = %w[atoms molecules organisms models
|
|
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[:
|
|
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
|
-
@
|
|
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
|
|
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 @
|
|
24
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
54
|
+
target_members.flat_map do |member|
|
|
45
55
|
member_key = member.to_s
|
|
46
56
|
|
|
47
|
-
if @
|
|
48
|
-
|
|
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
|
-
(@
|
|
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
|
-
|
|
69
|
-
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
116
|
+
target_members.flat_map do |member|
|
|
91
117
|
member_key = member.to_s
|
|
92
118
|
|
|
93
|
-
if @
|
|
94
|
-
|
|
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
|
-
#
|
|
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"
|
|
114
|
-
if @
|
|
115
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
68
|
+
# Check target_isolation config for sequential target execution
|
|
69
69
|
# true = subprocess for better isolation
|
|
70
|
-
if options[:
|
|
70
|
+
if options[:target_isolation] == true
|
|
71
71
|
return :subprocess
|
|
72
72
|
end
|
|
73
73
|
|
data/lib/ace/test_runner/organisms/{sequential_group_executor.rb → sequential_target_executor.rb}
RENAMED
|
@@ -3,28 +3,28 @@
|
|
|
3
3
|
module Ace
|
|
4
4
|
module TestRunner
|
|
5
5
|
module Organisms
|
|
6
|
-
# Executes test
|
|
7
|
-
class
|
|
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
|
|
15
|
-
fail_fast = options[:fail_fast] || options[:
|
|
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
|
-
|
|
18
|
+
target_results = []
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
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
|
|
26
|
-
if @formatter&.respond_to?(:
|
|
27
|
-
@formatter.
|
|
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
|
|
49
|
-
|
|
50
|
-
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
|
-
|
|
56
|
+
target_results << target_result
|
|
57
57
|
|
|
58
|
-
# Notify formatter of
|
|
59
|
-
if @formatter&.respond_to?(:
|
|
60
|
-
@formatter.
|
|
61
|
-
|
|
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
|
|
72
|
+
# Stop if target failed and fail_fast is enabled
|
|
73
73
|
if fail_fast && !success
|
|
74
|
-
|
|
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
|
|
80
|
-
build_aggregated_result(
|
|
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(
|
|
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
|
-
|
|
142
|
-
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 +=
|
|
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
|
-
|
|
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 "
|
|
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
|
|
66
|
+
# Check if sequential target execution should be used
|
|
67
67
|
if should_execute_sequentially?
|
|
68
|
-
# Use default "
|
|
69
|
-
@configuration.target ||= "
|
|
70
|
-
return
|
|
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 (
|
|
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
|
|
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
|
|
208
|
-
|
|
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
|
|
210
|
+
if targets.empty?
|
|
212
211
|
return handle_no_tests
|
|
213
212
|
end
|
|
214
213
|
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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[:
|
|
301
|
-
puts "\nSTOPPED:
|
|
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
|
-
|
|
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
|
|
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 "
|
|
326
|
-
target = @configuration.target || "
|
|
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.
|
|
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
|
-
|
|
342
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|