ace-test-runner 0.27.1 → 0.28.1
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/config.yml +2 -0
- data/CHANGELOG.md +27 -0
- data/exe/ace-test-suite +13 -4
- data/lib/ace/test_runner/atoms/command_builder.rb +60 -86
- data/lib/ace/test_runner/atoms/line_number_resolver.rb +70 -63
- data/lib/ace/test_runner/atoms/result_parser.rb +1 -1
- data/lib/ace/test_runner/cli/commands/test.rb +2 -3
- data/lib/ace/test_runner/formatters/json_formatter.rb +3 -1
- data/lib/ace/test_runner/formatters/progress_file_formatter.rb +10 -7
- data/lib/ace/test_runner/formatters/progress_formatter.rb +10 -7
- data/lib/ace/test_runner/models/test_report.rb +1 -0
- data/lib/ace/test_runner/models/test_result.rb +12 -3
- data/lib/ace/test_runner/models/test_selection_plan.rb +44 -0
- data/lib/ace/test_runner/molecules/cli_argument_parser.rb +9 -1
- data/lib/ace/test_runner/molecules/execution_evidence.rb +102 -0
- data/lib/ace/test_runner/molecules/failed_package_reporter.rb +4 -65
- data/lib/ace/test_runner/molecules/in_process_runner.rb +48 -37
- data/lib/ace/test_runner/molecules/pattern_resolver.rb +8 -5
- data/lib/ace/test_runner/molecules/report_storage.rb +24 -20
- data/lib/ace/test_runner/molecules/selection_resolver.rb +45 -0
- data/lib/ace/test_runner/molecules/selection_verifier.rb +78 -0
- data/lib/ace/test_runner/molecules/smart_test_executor.rb +4 -0
- data/lib/ace/test_runner/molecules/test_executor.rb +11 -4
- data/lib/ace/test_runner/organisms/sequential_target_executor.rb +6 -7
- data/lib/ace/test_runner/organisms/test_orchestrator.rb +44 -21
- data/lib/ace/test_runner/suite/display_manager.rb +7 -6
- data/lib/ace/test_runner/suite/orchestrator.rb +5 -3
- data/lib/ace/test_runner/suite/process_monitor.rb +37 -119
- data/lib/ace/test_runner/suite/result_aggregator.rb +17 -89
- data/lib/ace/test_runner/suite/simple_display_manager.rb +2 -1
- data/lib/ace/test_runner/version.rb +1 -1
- data/lib/ace/test_runner.rb +1 -0
- metadata +34 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8fc2c5132ba01f77ad703fc44426139e83ae2f930eb95a3ab26293f237bd13c7
|
|
4
|
+
data.tar.gz: b998e891eeecc4dd6ff6be3cdfcfb15c5d14a17333b306febecbd1db4dee4532
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d70486feda75375aaf8e5057adc1c6b9a10cf2692a7b2b7cd5d337c6375ba53a894c353de62b3178ca33e95460a208b367a9c283256b9ae8ee8127891f18e2bb
|
|
7
|
+
data.tar.gz: 2545709874b877010ad45bb538d05d03167b0a994a81119503a85c9f8666a13c689a2b971c738d8107d9f312785ec5a329de40718ac796baebb975f13f8421fa
|
|
@@ -26,6 +26,7 @@ patterns:
|
|
|
26
26
|
core: "test/{fast/,}core/**/*_test.rb"
|
|
27
27
|
lifecycle: "test/{fast/,}lifecycle/**/*_test.rb"
|
|
28
28
|
edge: "test/{fast/,}edge/**/*_test.rb"
|
|
29
|
+
fast_tests: "test/fast/**/*_test.rb"
|
|
29
30
|
feat_tests: "test/feat/**/*_test.rb"
|
|
30
31
|
|
|
31
32
|
# Test targets that combine multiple patterns
|
|
@@ -45,6 +46,7 @@ targets:
|
|
|
45
46
|
- contract
|
|
46
47
|
- core
|
|
47
48
|
- lifecycle
|
|
49
|
+
- fast_tests
|
|
48
50
|
feat:
|
|
49
51
|
- feat_tests
|
|
50
52
|
- edge
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.28.1] - 2026-10-08
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Reject missing explicit Ruby test files before loading tests or running selectors. Preserve exact unambiguous file-line selection.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Refuse invalid or ambiguous file-line selections and execute exact class/method identities in direct and subprocess modes. Reject mixed whole-file/line selections before loading tests.
|
|
19
|
+
|
|
20
|
+
- Include uncategorized fast test directories in default fast/all selection without executing overlapping matches twice or widening explicit target overrides.
|
|
21
|
+
- Exclude undispatched groups from sequential execution reports after target fail-fast.
|
|
22
|
+
|
|
23
|
+
## [0.28.0] - 2026-10-05
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Honor explicit `ace-test-suite --config FILE` without merging the default cascade, and refuse unreadable or malformed selected files. Default namespace discovery remains unchanged.
|
|
28
|
+
|
|
29
|
+
- Bind suite verdicts, counts and report links to each exact child invocation; preserve duplicate entries and no-save completion evidence during concurrent runs.
|
|
30
|
+
|
|
31
|
+
- Preserve unsuccessful execution through target aggregation and saved/CLI verdicts even after passing partial output; distinguish operator interruption from completed test errors.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Declare the required direct dependencies and minimum producer versions for this coordinated release: `ace-support-fs ~> 0.3`.
|
|
36
|
+
|
|
10
37
|
## [0.27.1] - 2026-09-29
|
|
11
38
|
|
|
12
39
|
### Fixed
|
data/exe/ace-test-suite
CHANGED
|
@@ -7,7 +7,7 @@ require "ace/test_runner"
|
|
|
7
7
|
require "ace/test_runner/suite"
|
|
8
8
|
|
|
9
9
|
options = {
|
|
10
|
-
config:
|
|
10
|
+
config: nil,
|
|
11
11
|
parallel: nil,
|
|
12
12
|
group: nil,
|
|
13
13
|
verbose: false,
|
|
@@ -87,11 +87,20 @@ rescue OptionParser::InvalidArgument => e
|
|
|
87
87
|
exit 1
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
#
|
|
91
|
-
|
|
90
|
+
# Explicit files replace the cascade; omitting --config keeps namespace discovery.
|
|
91
|
+
begin
|
|
92
|
+
config = if options[:config]
|
|
93
|
+
Ace::Support::Config::Molecules::YamlLoader.load_file(options[:config]).data
|
|
94
|
+
else
|
|
95
|
+
Ace::Core.get("test", file: "suite")
|
|
96
|
+
end
|
|
97
|
+
rescue Ace::Support::Config::Error, Psych::Exception => e
|
|
98
|
+
puts "Error: Configuration file #{options[:config] || '.ace/test/suite.yml'}: #{e.message}"
|
|
99
|
+
exit 1
|
|
100
|
+
end
|
|
92
101
|
|
|
93
102
|
unless config.is_a?(Hash) && config["test_suite"].is_a?(Hash)
|
|
94
|
-
puts "Error:
|
|
103
|
+
puts "Error: Invalid or missing test suite configuration: #{options[:config] || '.ace/test/suite.yml'}"
|
|
95
104
|
puts
|
|
96
105
|
puts "Create a test suite configuration at .ace/test/suite.yml in your project root"
|
|
97
106
|
puts
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "line_number_resolver"
|
|
4
|
+
require_relative "../molecules/selection_resolver"
|
|
5
|
+
require "shellwords"
|
|
4
6
|
|
|
5
7
|
module Ace
|
|
6
8
|
module TestRunner
|
|
@@ -13,6 +15,8 @@ module Ace
|
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def build_test_command(files, options = {})
|
|
18
|
+
selection = options[:selection_plan] || Molecules::SelectionResolver.resolve(Array(files))
|
|
19
|
+
return exact_selection_command(selection, options) if selection.qualified?
|
|
16
20
|
cmd_parts = []
|
|
17
21
|
|
|
18
22
|
# Use bundler if available and requested
|
|
@@ -29,39 +33,28 @@ module Ace
|
|
|
29
33
|
|
|
30
34
|
# Add the test files
|
|
31
35
|
if files.is_a?(Array)
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
script_parts << "exit_code = Minitest.autorun"
|
|
55
|
-
script_parts << "exit(exit_code)"
|
|
56
|
-
|
|
57
|
-
# Execute the requires and then run Minitest
|
|
58
|
-
cmd_parts << "-e"
|
|
59
|
-
# Use double quotes to wrap the entire script
|
|
60
|
-
cmd_parts << "\"#{script_parts.join("; ")}\""
|
|
61
|
-
end
|
|
62
|
-
elsif files.match?(/:\d+$/)
|
|
63
|
-
# Check if single file has line number
|
|
64
|
-
build_line_number_command(cmd_parts, [files], options)
|
|
36
|
+
# Build a Ruby script that requires each file and fails on LoadError
|
|
37
|
+
requires_script = files.map do |f|
|
|
38
|
+
# Add ./ prefix if it's a relative path without one
|
|
39
|
+
path = f.start_with?("/", "./") ? f : "./#{f}"
|
|
40
|
+
# Escape the path for shell safety
|
|
41
|
+
escaped_path = path.gsub("'", "\\\\'")
|
|
42
|
+
"begin; require '#{escaped_path}'; rescue LoadError => e; STDERR.puts \\\"Failed to load #{escaped_path}: \\\" + e.message; exit(1); end"
|
|
43
|
+
end.join("; ")
|
|
44
|
+
|
|
45
|
+
# Build the script parts
|
|
46
|
+
script_parts = []
|
|
47
|
+
# Inject ARGV with --verbose for Minitest if profile is requested
|
|
48
|
+
# (Ruby's --verbose flag only sets $VERBOSE, doesn't enable Minitest verbose mode)
|
|
49
|
+
script_parts << "ARGV.replace(['--verbose'])" if options[:profile]
|
|
50
|
+
script_parts << requires_script
|
|
51
|
+
script_parts << "exit_code = Minitest.autorun"
|
|
52
|
+
script_parts << "exit(exit_code)"
|
|
53
|
+
|
|
54
|
+
# Execute the requires and then run Minitest
|
|
55
|
+
cmd_parts << "-e"
|
|
56
|
+
# Use double quotes to wrap the entire script
|
|
57
|
+
cmd_parts << "\"#{script_parts.join("; ")}\""
|
|
65
58
|
elsif options[:profile]
|
|
66
59
|
# Single file without line number
|
|
67
60
|
cmd_parts << "-e"
|
|
@@ -100,60 +93,41 @@ module Ace
|
|
|
100
93
|
|
|
101
94
|
private
|
|
102
95
|
|
|
103
|
-
def
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
96
|
+
def exact_selection_command(plan, options)
|
|
97
|
+
argv = []
|
|
98
|
+
argv.concat(["bundle", "exec"]) if @bundler && bundler_available?
|
|
99
|
+
argv.concat(Shellwords.split(@ruby_command))
|
|
100
|
+
argv << "-Ilib:test" unless options[:no_load_path]
|
|
101
|
+
model = File.expand_path("../models/test_selection_plan", __dir__)
|
|
102
|
+
verifier = File.expand_path("../molecules/selection_verifier", __dir__)
|
|
103
|
+
script = <<~RUBY
|
|
104
|
+
begin
|
|
105
|
+
require #{model.inspect}
|
|
106
|
+
require #{verifier.inspect}
|
|
107
|
+
plan = Ace::TestRunner::Models::TestSelectionPlan.new(
|
|
108
|
+
files: #{plan.files.inspect}, identities: #{plan.identities.inspect},
|
|
109
|
+
source_digests: #{plan.source_digests.inspect})
|
|
110
|
+
verifier = Ace::TestRunner::Molecules::SelectionVerifier
|
|
111
|
+
verifier.verify_sources!(plan)
|
|
112
|
+
require "minitest/autorun"
|
|
113
|
+
plan.files.each { |file| require file }
|
|
114
|
+
pattern = verifier.verify_loaded!(plan)
|
|
115
|
+
args = ["--name", "/" + pattern + "/"]
|
|
116
|
+
args << "--verbose" if #{!!options[:profile]}
|
|
117
|
+
Minitest::Reporters.use! Minitest::Reporters::DefaultReporter.new
|
|
118
|
+
success = verifier.verify_execution!(plan) { Minitest.run(args) }
|
|
119
|
+
STDOUT.flush
|
|
120
|
+
STDERR.flush
|
|
121
|
+
exit!(success ? 0 : 1)
|
|
122
|
+
rescue Exception => error
|
|
123
|
+
STDERR.puts(error.message)
|
|
124
|
+
STDERR.flush
|
|
125
|
+
exit!(1)
|
|
130
126
|
end
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
# Require all files
|
|
137
|
-
script_parts << file_requires.join("; ")
|
|
138
|
-
|
|
139
|
-
# Set up ARGV with --name filter and --verbose if needed
|
|
140
|
-
argv_args = []
|
|
141
|
-
argv_args << "--verbose" if options[:profile]
|
|
142
|
-
if test_names.any?
|
|
143
|
-
# Create a regex pattern that matches any of the test names
|
|
144
|
-
pattern = test_names.map { |name| Regexp.escape(name) }.join("|")
|
|
145
|
-
argv_args << "--name"
|
|
146
|
-
argv_args << "/#{pattern}/"
|
|
147
|
-
end
|
|
148
|
-
script_parts << "ARGV.replace([#{argv_args.map { |a| "'#{a}'" }.join(", ")}])" if argv_args.any?
|
|
149
|
-
|
|
150
|
-
# Run Minitest
|
|
151
|
-
script_parts << "exit_code = Minitest.autorun"
|
|
152
|
-
script_parts << "exit(exit_code)"
|
|
153
|
-
|
|
154
|
-
# Add to command
|
|
155
|
-
cmd_parts << "-e"
|
|
156
|
-
cmd_parts << "\"#{script_parts.join("; ")}\""
|
|
127
|
+
RUBY
|
|
128
|
+
argv.concat(["-e", script])
|
|
129
|
+
argv.concat(Array(options[:args])) if options[:args]
|
|
130
|
+
argv
|
|
157
131
|
end
|
|
158
132
|
|
|
159
133
|
def bundler_available?
|
|
@@ -1,85 +1,92 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
3
5
|
module Ace
|
|
4
6
|
module TestRunner
|
|
5
7
|
module Atoms
|
|
6
|
-
# Resolves line numbers to test method names
|
|
7
8
|
module LineNumberResolver
|
|
9
|
+
class SelectionError < ArgumentError; end
|
|
8
10
|
module_function
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
test_methods = extract_test_methods(lines)
|
|
20
|
-
|
|
21
|
-
# Find the test that contains the specified line
|
|
22
|
-
test_methods.find do |test|
|
|
23
|
-
line_number >= test[:start_line] && line_number <= test[:end_line]
|
|
24
|
-
end&.fetch(:name)
|
|
12
|
+
def parse_file_with_line(selector)
|
|
13
|
+
if (match = /\A(.+\.rb):(.+)\z/.match(selector))
|
|
14
|
+
unless match[2].match?(/\A[1-9]\d*\z/)
|
|
15
|
+
raise SelectionError, "Invalid test selector: #{selector}"
|
|
16
|
+
end
|
|
17
|
+
{file: match[1], line: Integer(match[2], 10)}
|
|
18
|
+
else
|
|
19
|
+
{file: selector, line: nil}
|
|
20
|
+
end
|
|
25
21
|
end
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
current_test = nil
|
|
31
|
-
|
|
32
|
-
lines.each_with_index do |line, index|
|
|
33
|
-
line_number = index + 1
|
|
34
|
-
|
|
35
|
-
# Match test method definitions: def test_something or test "something"
|
|
36
|
-
if line =~ /^\s*(def\s+(test_\w+)|test\s+["'](.+)["']\s+do)/
|
|
37
|
-
test_name = $2 || $3 # Either def test_name or test "name"
|
|
38
|
-
|
|
39
|
-
# Convert test "name" to test_name format for minitest --name option
|
|
40
|
-
test_name = test_name.gsub(/\s+/, "_") if test_name && test_name.include?(" ")
|
|
41
|
-
|
|
42
|
-
# Close previous test if any
|
|
43
|
-
if current_test
|
|
44
|
-
current_test[:end_line] = line_number - 1
|
|
45
|
-
test_methods << current_test
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
current_test = {
|
|
49
|
-
name: test_name,
|
|
50
|
-
start_line: line_number,
|
|
51
|
-
end_line: lines.size # Default to end of file
|
|
52
|
-
}
|
|
53
|
-
elsif line =~ /^\s*end\s*(#.*)?$/ && current_test
|
|
54
|
-
# Found an end keyword - could be end of test method
|
|
55
|
-
# Simple heuristic: if we're at the same or less indentation level, close the test
|
|
56
|
-
current_indent = line[/^\s*/].length
|
|
23
|
+
def resolve_test_at_line(file_path, line_number)
|
|
24
|
+
resolve_identity(file_path, line_number).fetch(:name)
|
|
25
|
+
end
|
|
57
26
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
end
|
|
27
|
+
def resolve_identity(file_path, line_number, source: nil)
|
|
28
|
+
selector = "#{file_path}:#{line_number}"
|
|
29
|
+
source ||= File.read(file_path)
|
|
30
|
+
unless line_number.is_a?(Integer) && line_number.positive? && line_number <= source.lines.size
|
|
31
|
+
raise SelectionError, "Invalid test selector: #{selector}"
|
|
64
32
|
end
|
|
33
|
+
parsed = Prism.parse(source)
|
|
34
|
+
raise SelectionError, "Malformed Ruby in test selector: #{selector}" unless parsed.success?
|
|
65
35
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
36
|
+
declarations = []
|
|
37
|
+
scopes = Hash.new(0)
|
|
38
|
+
collect(parsed.value, [], false, true, declarations, scopes)
|
|
39
|
+
matches = declarations.select { |entry| (entry[:start_line]..entry[:end_line]).cover?(line_number) }
|
|
40
|
+
selected = matches.one? && matches.first
|
|
41
|
+
valid = selected && selected[:supported] && scopes[selected[:class_name]] == 1 &&
|
|
42
|
+
declarations.count { |entry| entry.values_at(:class_name, :name) == selected.values_at(:class_name, :name) } == 1
|
|
43
|
+
raise SelectionError, "Unmatched or ambiguous test selector: #{selector}" unless valid
|
|
71
44
|
|
|
72
|
-
|
|
45
|
+
selected.reject { |key, _| key == :supported }.merge(file: File.expand_path(file_path)).freeze
|
|
46
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::EISDIR => error
|
|
47
|
+
raise SelectionError, "Cannot read test selector #{selector}: #{error.class}"
|
|
73
48
|
end
|
|
74
49
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
50
|
+
def collect(node, namespace, in_class, supported, declarations, scopes)
|
|
51
|
+
return unless node
|
|
52
|
+
case node
|
|
53
|
+
when Prism::ProgramNode
|
|
54
|
+
collect(node.statements, namespace, in_class, supported, declarations, scopes)
|
|
55
|
+
when Prism::StatementsNode
|
|
56
|
+
node.body.each { |child| collect(child, namespace, in_class, supported, declarations, scopes) }
|
|
57
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
58
|
+
text = node.constant_path.location.slice
|
|
59
|
+
static = text.match?(/\A(?:::)?[A-Z]\w*(?:::[A-Z]\w*)*\z/)
|
|
60
|
+
parts = static ? text.delete_prefix("::").split("::") : []
|
|
61
|
+
path = text.start_with?("::") ? parts : namespace + parts
|
|
62
|
+
scopes[path.join("::")] += 1
|
|
63
|
+
collect(node.body, path, node.is_a?(Prism::ClassNode), supported && static, declarations, scopes)
|
|
64
|
+
when Prism::DefNode
|
|
65
|
+
if node.name.to_s.start_with?("test_")
|
|
66
|
+
add_declaration(node, node.name.to_s, namespace, supported && in_class && node.receiver.nil?,
|
|
67
|
+
node.def_keyword_loc.start_line, declarations)
|
|
68
|
+
end
|
|
69
|
+
# Nested declarations cannot acquire the surrounding static class's identity.
|
|
70
|
+
collect(node.body, namespace, in_class, false, declarations, scopes)
|
|
71
|
+
when Prism::CallNode
|
|
72
|
+
if node.name == :test && node.block.is_a?(Prism::BlockNode)
|
|
73
|
+
arguments = node.arguments&.arguments || []
|
|
74
|
+
literal = arguments.one? && arguments.first.is_a?(Prism::StringNode)
|
|
75
|
+
name = literal ? "test_#{arguments.first.unescaped.gsub(/\s+/, '_')}" : nil
|
|
76
|
+
static = supported && in_class && node.receiver.nil? && literal && node.block.opening_loc.slice == "do"
|
|
77
|
+
add_declaration(node, name, namespace, static, node.block.opening_loc.start_line, declarations)
|
|
78
|
+
end
|
|
79
|
+
node.compact_child_nodes.each { |child| collect(child, namespace, in_class, false, declarations, scopes) }
|
|
79
80
|
else
|
|
80
|
-
{
|
|
81
|
+
node.compact_child_nodes.each { |child| collect(child, namespace, in_class, false, declarations, scopes) }
|
|
81
82
|
end
|
|
82
83
|
end
|
|
84
|
+
|
|
85
|
+
def add_declaration(node, name, namespace, supported, method_line, declarations)
|
|
86
|
+
declarations << {name: name, class_name: namespace.join("::"), start_line: node.location.start_line,
|
|
87
|
+
end_line: node.location.end_line, method_line: method_line, supported: supported}
|
|
88
|
+
end
|
|
89
|
+
private_class_method :collect, :add_declaration
|
|
83
90
|
end
|
|
84
91
|
end
|
|
85
92
|
end
|
|
@@ -15,7 +15,7 @@ module Ace
|
|
|
15
15
|
# Pattern for inline errors - same structure as failures but with ERROR marker
|
|
16
16
|
inline_error: /^\s+(test_[\w_]+).*?ERROR.*?\([\d.]+s\)\n(.*?)(?=^\s+test_[\w_]+.*?(?:PASS|FAIL|ERROR|SKIP)|^Finished in|\z)/m,
|
|
17
17
|
location: /\[(.*?):(\d+)\]/,
|
|
18
|
-
duration: /Finished in ([\d.]+)s/,
|
|
18
|
+
duration: /Finished(?: tests)? in ([\d.]+)s/,
|
|
19
19
|
deprecation: /DEPRECATION WARNING: (.+)/,
|
|
20
20
|
# Pattern to capture individual test times from verbose output
|
|
21
21
|
# Matches Minitest::Reporters DefaultReporter format:
|
|
@@ -90,6 +90,7 @@ module Ace
|
|
|
90
90
|
# Execution mode options
|
|
91
91
|
option :parallel, type: :boolean, desc: "Run tests in parallel (experimental)"
|
|
92
92
|
option :per_file, type: :boolean, desc: "Execute each test file separately (slower, for debugging)"
|
|
93
|
+
# File-line selectors resolve exact declarations; mixed whole-file/line input refuses.
|
|
93
94
|
option :direct, type: :boolean, desc: "Force in-process execution (faster, less isolation)"
|
|
94
95
|
option :subprocess, type: :boolean, desc: "Force subprocess execution (slower, full isolation)"
|
|
95
96
|
option :run_in_sequence, type: :boolean, aliases: %w[--ris], desc: "Run test targets sequentially (default)"
|
|
@@ -221,10 +222,8 @@ module Ace
|
|
|
221
222
|
config_data = config_loader.load(options[:config_path])
|
|
222
223
|
config = config_loader.merge_with_options(config_data, options)
|
|
223
224
|
|
|
224
|
-
timestamp_generator = Atoms::TimestampGenerator.new
|
|
225
225
|
storage = Molecules::ReportStorage.new(
|
|
226
|
-
base_dir: config[:defaults][:report_dir] || ".ace-local/test/reports"
|
|
227
|
-
timestamp_generator: timestamp_generator
|
|
226
|
+
base_dir: config[:defaults][:report_dir] || ".ace-local/test/reports"
|
|
228
227
|
)
|
|
229
228
|
|
|
230
229
|
keep = options[:cleanup_keep] || 10
|
|
@@ -53,7 +53,9 @@ module Ace
|
|
|
53
53
|
assertions: result.assertions,
|
|
54
54
|
duration: result.duration,
|
|
55
55
|
pass_rate: result.pass_rate,
|
|
56
|
-
success: result.success
|
|
56
|
+
success: result.success?,
|
|
57
|
+
execution_success: result.execution_success,
|
|
58
|
+
execution_error: result.execution_error
|
|
57
59
|
},
|
|
58
60
|
timing: {
|
|
59
61
|
start_time: result.start_time&.iso8601,
|
|
@@ -24,8 +24,8 @@ module Ace
|
|
|
24
24
|
# Report directory - use actual report path if available
|
|
25
25
|
if @report_path
|
|
26
26
|
lines << "Details: #{@report_path}/"
|
|
27
|
-
|
|
28
|
-
lines << "
|
|
27
|
+
else
|
|
28
|
+
lines << "No saved report"
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
# Compact single-line summary with emoji status
|
|
@@ -42,7 +42,9 @@ module Ace
|
|
|
42
42
|
lines << summary
|
|
43
43
|
|
|
44
44
|
# Add failure details if there are any
|
|
45
|
-
|
|
45
|
+
lines << "Execution failed: #{result.execution_error}" unless result.execution_success
|
|
46
|
+
|
|
47
|
+
if result.failed > 0 || result.errors > 0
|
|
46
48
|
lines << ""
|
|
47
49
|
total_failures = result.failed + result.errors
|
|
48
50
|
|
|
@@ -51,8 +53,8 @@ module Ace
|
|
|
51
53
|
|
|
52
54
|
# Show failure count header with reference to full report if needed
|
|
53
55
|
if total_failures > @max_failures_to_display
|
|
54
|
-
|
|
55
|
-
lines << "FAILURES (#{failures_to_show.size}/#{total_failures})
|
|
56
|
+
reference = @report_path ? "→ #{@report_path}/failures.json" : "(no saved report)"
|
|
57
|
+
lines << "FAILURES (#{failures_to_show.size}/#{total_failures}) #{reference}:"
|
|
56
58
|
else
|
|
57
59
|
lines << "FAILURES (#{total_failures}):"
|
|
58
60
|
end
|
|
@@ -87,8 +89,9 @@ module Ace
|
|
|
87
89
|
# If there are more failures than displayed
|
|
88
90
|
if result.failures_detail.size > @max_failures_to_display
|
|
89
91
|
remaining = result.failures_detail.size - @max_failures_to_display
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
reference = @report_path ? "See full report: #{@report_path}/failures.json" : "No saved report"
|
|
93
|
+
noun = remaining == 1 ? "failure" : "failures"
|
|
94
|
+
lines << " ... and #{remaining} more #{noun}. #{reference}"
|
|
92
95
|
end
|
|
93
96
|
end
|
|
94
97
|
|
|
@@ -29,8 +29,8 @@ module Ace
|
|
|
29
29
|
# Report directory - use actual report path if available
|
|
30
30
|
if @report_path
|
|
31
31
|
lines << "Details: #{@report_path}/"
|
|
32
|
-
|
|
33
|
-
lines << "
|
|
32
|
+
else
|
|
33
|
+
lines << "No saved report"
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
# Compact single-line summary with emoji status
|
|
@@ -43,7 +43,9 @@ module Ace
|
|
|
43
43
|
lines << summary
|
|
44
44
|
|
|
45
45
|
# Add failure details if there are any
|
|
46
|
-
|
|
46
|
+
lines << "Execution failed: #{result.execution_error}" unless result.execution_success
|
|
47
|
+
|
|
48
|
+
if result.failed > 0 || result.errors > 0
|
|
47
49
|
lines << ""
|
|
48
50
|
total_failures = result.failed + result.errors
|
|
49
51
|
|
|
@@ -64,8 +66,8 @@ module Ace
|
|
|
64
66
|
|
|
65
67
|
# Show failure count header with reference to full report if needed
|
|
66
68
|
if total_failures > @max_failures_to_display
|
|
67
|
-
|
|
68
|
-
lines << "#{label} (#{failures_to_show.size}/#{total_failures})
|
|
69
|
+
reference = @report_path ? "→ #{@report_path}/failures.json" : "(no saved report)"
|
|
70
|
+
lines << "#{label} (#{failures_to_show.size}/#{total_failures}) #{reference}:"
|
|
69
71
|
else
|
|
70
72
|
lines << "#{label} (#{total_failures}):"
|
|
71
73
|
end
|
|
@@ -100,8 +102,9 @@ module Ace
|
|
|
100
102
|
# If there are more failures than displayed
|
|
101
103
|
if result.failures_detail.size > @max_failures_to_display
|
|
102
104
|
remaining = result.failures_detail.size - @max_failures_to_display
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
reference = @report_path ? "See full report: #{@report_path}/failures.json" : "No saved report"
|
|
106
|
+
noun = remaining == 1 ? "failure" : "failures"
|
|
107
|
+
lines << " ... and #{remaining} more #{noun}. #{reference}"
|
|
105
108
|
end
|
|
106
109
|
end
|
|
107
110
|
|
|
@@ -76,6 +76,7 @@ module Ace
|
|
|
76
76
|
lines << ""
|
|
77
77
|
lines << "**Generated:** #{timestamp.strftime("%Y-%m-%d %H:%M:%S")}"
|
|
78
78
|
lines << "**Status:** #{success? ? "✅ Success" : "❌ Failed"}"
|
|
79
|
+
lines << "**Execution failure:** #{result.execution_error}" unless result.execution_success
|
|
79
80
|
lines << ""
|
|
80
81
|
|
|
81
82
|
lines << "## Summary"
|
|
@@ -7,7 +7,7 @@ module Ace
|
|
|
7
7
|
class TestResult
|
|
8
8
|
attr_accessor :passed, :failed, :skipped, :errors, :assertions,
|
|
9
9
|
:duration, :start_time, :end_time, :failures_detail,
|
|
10
|
-
:deprecations, :raw_output, :stderr
|
|
10
|
+
:deprecations, :raw_output, :stderr, :execution_success
|
|
11
11
|
|
|
12
12
|
def initialize(attributes = {})
|
|
13
13
|
@passed = attributes[:passed] || 0
|
|
@@ -22,6 +22,7 @@ module Ace
|
|
|
22
22
|
@deprecations = attributes[:deprecations] || []
|
|
23
23
|
@raw_output = attributes[:raw_output] || ""
|
|
24
24
|
@stderr = attributes[:stderr] || ""
|
|
25
|
+
@execution_success = attributes.fetch(:execution_success, true) == true
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def total_tests
|
|
@@ -29,11 +30,16 @@ module Ace
|
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def success?
|
|
32
|
-
failed == 0 && errors == 0
|
|
33
|
+
execution_success && failed == 0 && errors == 0
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
def has_failures?
|
|
36
|
-
|
|
37
|
+
!success?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def execution_error
|
|
41
|
+
return nil if execution_success
|
|
42
|
+
stderr.to_s.strip.empty? ? "Test execution did not complete successfully" : stderr.to_s.strip
|
|
37
43
|
end
|
|
38
44
|
|
|
39
45
|
def has_skips?
|
|
@@ -55,6 +61,7 @@ module Ace
|
|
|
55
61
|
parts << "❌ #{failed} failed" if failed > 0
|
|
56
62
|
parts << "💥 #{errors} errors" if errors > 0
|
|
57
63
|
parts << "⚠️ #{skipped} skipped" if skipped > 0
|
|
64
|
+
parts << "❌ execution failed" unless execution_success
|
|
58
65
|
|
|
59
66
|
parts.empty? ? "No tests executed" : parts.join(", ")
|
|
60
67
|
end
|
|
@@ -70,6 +77,8 @@ module Ace
|
|
|
70
77
|
duration: duration,
|
|
71
78
|
pass_rate: pass_rate,
|
|
72
79
|
success: success?,
|
|
80
|
+
execution_success: execution_success,
|
|
81
|
+
execution_error: execution_error,
|
|
73
82
|
start_time: start_time&.iso8601,
|
|
74
83
|
end_time: end_time&.iso8601,
|
|
75
84
|
failures: failures_detail.map(&:to_h),
|