ace-test-runner 0.18.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.
- checksums.yaml +7 -0
- data/.ace-defaults/test/runner.yml +35 -0
- data/.ace-defaults/test/suite.yml +31 -0
- data/.ace-defaults/test-runner/config.yml +61 -0
- data/CHANGELOG.md +626 -0
- data/LICENSE +21 -0
- data/README.md +42 -0
- data/Rakefile +14 -0
- data/exe/ace-test +26 -0
- data/exe/ace-test-suite +149 -0
- data/lib/ace/test_runner/atoms/command_builder.rb +165 -0
- data/lib/ace/test_runner/atoms/lazy_loader.rb +62 -0
- data/lib/ace/test_runner/atoms/line_number_resolver.rb +86 -0
- data/lib/ace/test_runner/atoms/report_directory_resolver.rb +48 -0
- data/lib/ace/test_runner/atoms/report_path_resolver.rb +67 -0
- data/lib/ace/test_runner/atoms/result_parser.rb +254 -0
- data/lib/ace/test_runner/atoms/test_detector.rb +114 -0
- data/lib/ace/test_runner/atoms/test_folder_detector.rb +53 -0
- data/lib/ace/test_runner/atoms/test_type_detector.rb +83 -0
- data/lib/ace/test_runner/atoms/timestamp_generator.rb +103 -0
- data/lib/ace/test_runner/cli/commands/test.rb +326 -0
- data/lib/ace/test_runner/cli.rb +16 -0
- data/lib/ace/test_runner/formatters/base_formatter.rb +102 -0
- data/lib/ace/test_runner/formatters/json_formatter.rb +90 -0
- data/lib/ace/test_runner/formatters/markdown_formatter.rb +91 -0
- data/lib/ace/test_runner/formatters/progress_file_formatter.rb +164 -0
- data/lib/ace/test_runner/formatters/progress_formatter.rb +328 -0
- data/lib/ace/test_runner/models/test_configuration.rb +165 -0
- data/lib/ace/test_runner/models/test_failure.rb +95 -0
- data/lib/ace/test_runner/models/test_group.rb +105 -0
- data/lib/ace/test_runner/models/test_report.rb +145 -0
- data/lib/ace/test_runner/models/test_result.rb +86 -0
- data/lib/ace/test_runner/molecules/cli_argument_parser.rb +263 -0
- data/lib/ace/test_runner/molecules/config_loader.rb +162 -0
- data/lib/ace/test_runner/molecules/deprecation_fixer.rb +204 -0
- data/lib/ace/test_runner/molecules/failed_package_reporter.rb +100 -0
- data/lib/ace/test_runner/molecules/failure_analyzer.rb +249 -0
- data/lib/ace/test_runner/molecules/in_process_runner.rb +249 -0
- data/lib/ace/test_runner/molecules/package_resolver.rb +106 -0
- data/lib/ace/test_runner/molecules/pattern_resolver.rb +146 -0
- data/lib/ace/test_runner/molecules/rake_integration.rb +218 -0
- data/lib/ace/test_runner/molecules/report_storage.rb +303 -0
- data/lib/ace/test_runner/molecules/smart_test_executor.rb +107 -0
- data/lib/ace/test_runner/molecules/test_executor.rb +162 -0
- data/lib/ace/test_runner/organisms/agent_reporter.rb +384 -0
- data/lib/ace/test_runner/organisms/report_generator.rb +151 -0
- data/lib/ace/test_runner/organisms/sequential_group_executor.rb +185 -0
- data/lib/ace/test_runner/organisms/test_orchestrator.rb +648 -0
- data/lib/ace/test_runner/rake_task.rb +90 -0
- data/lib/ace/test_runner/suite/display_helpers.rb +117 -0
- data/lib/ace/test_runner/suite/display_manager.rb +204 -0
- data/lib/ace/test_runner/suite/duration_estimator.rb +50 -0
- data/lib/ace/test_runner/suite/orchestrator.rb +120 -0
- data/lib/ace/test_runner/suite/process_monitor.rb +268 -0
- data/lib/ace/test_runner/suite/result_aggregator.rb +176 -0
- data/lib/ace/test_runner/suite/simple_display_manager.rb +122 -0
- data/lib/ace/test_runner/suite.rb +22 -0
- data/lib/ace/test_runner/version.rb +7 -0
- data/lib/ace/test_runner.rb +69 -0
- metadata +246 -0
data/Rakefile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "minitest/test_task"
|
|
5
|
+
|
|
6
|
+
desc "Run tests using ace-test"
|
|
7
|
+
task :test do
|
|
8
|
+
sh "ace-test"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
desc "Run tests directly (CI mode)"
|
|
12
|
+
Minitest::TestTask.create(:ci)
|
|
13
|
+
|
|
14
|
+
task default: :test
|
data/exe/ace-test
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Prevent Minitest from autorunning when ace-test exits
|
|
5
|
+
ENV["MT_NO_AUTORUN"] = "1"
|
|
6
|
+
|
|
7
|
+
require_relative "../lib/ace/test_runner"
|
|
8
|
+
|
|
9
|
+
# Start ace-support-cli with exception-based exit code handling (per ADR-023)
|
|
10
|
+
# IMPORTANT: Use exit! (not exit) to skip Ruby's at_exit handlers.
|
|
11
|
+
# This prevents Minitest from auto-running again via its at_exit hook.
|
|
12
|
+
# When ace-test uses in-process (direct) execution, test files are loaded
|
|
13
|
+
# into the current process. Minitest registers an at_exit handler that would
|
|
14
|
+
# re-run all tests on normal exit. Using exit! bypasses this.
|
|
15
|
+
# See guide://testable-code-patterns for details on this pattern.
|
|
16
|
+
begin
|
|
17
|
+
exit_code = Ace::Support::Cli::Runner.new(Ace::TestRunner::CLI::Commands::Test).call(args: ARGV)
|
|
18
|
+
$stdout.flush
|
|
19
|
+
$stderr.flush
|
|
20
|
+
exit!(exit_code.respond_to?(:to_i) ? exit_code.to_i : 0)
|
|
21
|
+
rescue Ace::Support::Cli::Error => e
|
|
22
|
+
warn e.message
|
|
23
|
+
$stdout.flush
|
|
24
|
+
$stderr.flush
|
|
25
|
+
exit!(e.exit_code)
|
|
26
|
+
end
|
data/exe/ace-test-suite
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "ace/core"
|
|
6
|
+
require "ace/test_runner"
|
|
7
|
+
require "ace/test_runner/suite"
|
|
8
|
+
|
|
9
|
+
options = {
|
|
10
|
+
config: ".ace/test/suite.yml",
|
|
11
|
+
parallel: nil,
|
|
12
|
+
group: nil,
|
|
13
|
+
verbose: false,
|
|
14
|
+
progress: false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
parser = OptionParser.new do |opts|
|
|
18
|
+
# Custom banner matching ace-support-cli ALL-CAPS style
|
|
19
|
+
opts.banner = <<~BANNER.chomp
|
|
20
|
+
NAME
|
|
21
|
+
ace-test-suite - Run all tests across the monorepo
|
|
22
|
+
|
|
23
|
+
USAGE
|
|
24
|
+
ace-test-suite [OPTIONS]
|
|
25
|
+
|
|
26
|
+
OPTIONS
|
|
27
|
+
BANNER
|
|
28
|
+
|
|
29
|
+
opts.on("-c", "--config FILE", "Configuration file (default: .ace/test/suite.yml)") do |config|
|
|
30
|
+
options[:config] = config
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
opts.on("-p", "--parallel N", Integer, "Maximum parallel processes") do |n|
|
|
34
|
+
options[:parallel] = n
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
opts.on("-g", "--group GROUP", "Run only packages in specified group") do |group|
|
|
38
|
+
options[:group] = group
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
opts.on("-v", "--verbose", "Show verbose output") do
|
|
42
|
+
options[:verbose] = true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
opts.on("--progress", "Enable live animated display with progress bars") do
|
|
46
|
+
options[:progress] = true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
opts.on("--no-color", "Disable colored output") do
|
|
50
|
+
options[:no_color] = true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
opts.on("-h", "--help", "Show this help") do
|
|
54
|
+
puts opts
|
|
55
|
+
puts
|
|
56
|
+
puts "EXAMPLES"
|
|
57
|
+
puts " $ ace-test-suite Run all (simple line-by-line output)"
|
|
58
|
+
puts " $ ace-test-suite --progress Show live animated progress bars"
|
|
59
|
+
puts " $ ace-test-suite --parallel 16 Run with 16 parallel processes"
|
|
60
|
+
puts " $ ace-test-suite --group foundation Run only foundation group"
|
|
61
|
+
puts " $ ace-test-suite --verbose Show detailed test output"
|
|
62
|
+
exit 0
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
opts.on("--version", "Show version") do
|
|
66
|
+
puts "ace-test-suite #{Ace::TestRunner::VERSION}"
|
|
67
|
+
exit 0
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
begin
|
|
72
|
+
parser.parse!(ARGV)
|
|
73
|
+
rescue OptionParser::InvalidArgument => e
|
|
74
|
+
puts "Error: #{e.message}"
|
|
75
|
+
puts
|
|
76
|
+
puts parser
|
|
77
|
+
exit 1
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Load the config using namespace method
|
|
81
|
+
config = Ace::Core.get("test", file: "suite")
|
|
82
|
+
|
|
83
|
+
unless config.is_a?(Hash) && config["test_suite"].is_a?(Hash)
|
|
84
|
+
puts "Error: Configuration file not found: .ace/test/suite.yml"
|
|
85
|
+
puts
|
|
86
|
+
puts "Create a test suite configuration at .ace/test/suite.yml in your project root"
|
|
87
|
+
puts
|
|
88
|
+
puts "Example configuration:"
|
|
89
|
+
puts <<~YAML
|
|
90
|
+
test_suite:
|
|
91
|
+
max_parallel: 10
|
|
92
|
+
packages:
|
|
93
|
+
- name: ace-support-core
|
|
94
|
+
path: ./ace-support-core
|
|
95
|
+
group: foundation
|
|
96
|
+
priority: 1
|
|
97
|
+
- name: ace-test-runner
|
|
98
|
+
path: ./ace-test-runner
|
|
99
|
+
group: foundation
|
|
100
|
+
priority: 1
|
|
101
|
+
test_options:
|
|
102
|
+
format: compact
|
|
103
|
+
save_reports: true
|
|
104
|
+
YAML
|
|
105
|
+
exit 1
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Override config with command line options if provided
|
|
109
|
+
if options[:parallel]
|
|
110
|
+
config["test_suite"]["max_parallel"] = options[:parallel]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
if options[:group]
|
|
114
|
+
config["test_suite"]["packages"].select! { |p| p["group"] == options[:group] }
|
|
115
|
+
if config["test_suite"]["packages"].empty?
|
|
116
|
+
puts "Error: No packages found in group '#{options[:group]}'"
|
|
117
|
+
exit 1
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if options[:no_color]
|
|
122
|
+
config["test_suite"]["display"] ||= {}
|
|
123
|
+
config["test_suite"]["display"]["color"] = false
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
if options[:verbose]
|
|
127
|
+
config["test_suite"]["test_options"] ||= {}
|
|
128
|
+
config["test_suite"]["test_options"]["verbose"] = true
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if options[:progress]
|
|
132
|
+
config["test_suite"]["progress"] = true
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Run the test suite with the config
|
|
136
|
+
begin
|
|
137
|
+
exit_code = Ace::TestRunner::Suite.run(config)
|
|
138
|
+
exit exit_code
|
|
139
|
+
rescue Ace::TestRunner::Suite::Error => e
|
|
140
|
+
puts "Error: #{e.message}"
|
|
141
|
+
exit 1
|
|
142
|
+
rescue Interrupt
|
|
143
|
+
puts "\n\nTest suite execution interrupted"
|
|
144
|
+
exit 130
|
|
145
|
+
rescue => e
|
|
146
|
+
puts "Unexpected error: #{e.message}"
|
|
147
|
+
puts e.backtrace if options[:verbose]
|
|
148
|
+
exit 1
|
|
149
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "line_number_resolver"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module TestRunner
|
|
7
|
+
module Atoms
|
|
8
|
+
# Builds test execution commands
|
|
9
|
+
class CommandBuilder
|
|
10
|
+
def initialize(ruby_command: "ruby", bundler: true)
|
|
11
|
+
@ruby_command = ruby_command
|
|
12
|
+
@bundler = bundler
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build_test_command(files, options = {})
|
|
16
|
+
cmd_parts = []
|
|
17
|
+
|
|
18
|
+
# Use bundler if available and requested
|
|
19
|
+
cmd_parts << "bundle exec" if @bundler && bundler_available?
|
|
20
|
+
|
|
21
|
+
# Ruby command
|
|
22
|
+
cmd_parts << @ruby_command
|
|
23
|
+
|
|
24
|
+
# Add test framework options
|
|
25
|
+
cmd_parts << "-Ilib:test" unless options[:no_load_path]
|
|
26
|
+
|
|
27
|
+
# Note: fail_fast is handled by test executor, not minitest
|
|
28
|
+
# We don't use minitest/fail_fast gem to avoid extra dependencies
|
|
29
|
+
|
|
30
|
+
# Add the test files
|
|
31
|
+
if files.is_a?(Array)
|
|
32
|
+
# Check if any file has a line number (file:line format)
|
|
33
|
+
has_line_numbers = files.any? { |f| f.match?(/:\d+$/) }
|
|
34
|
+
|
|
35
|
+
if has_line_numbers
|
|
36
|
+
# For files with line numbers, resolve to test names and filter
|
|
37
|
+
build_line_number_command(cmd_parts, files, options)
|
|
38
|
+
else
|
|
39
|
+
# Build a Ruby script that requires each file and fails on LoadError
|
|
40
|
+
requires_script = files.map do |f|
|
|
41
|
+
# Add ./ prefix if it's a relative path without one
|
|
42
|
+
path = f.start_with?("/", "./") ? f : "./#{f}"
|
|
43
|
+
# Escape the path for shell safety
|
|
44
|
+
escaped_path = path.gsub("'", "\\\\'")
|
|
45
|
+
"begin; require '#{escaped_path}'; rescue LoadError => e; STDERR.puts \\\"Failed to load #{escaped_path}: \\\" + e.message; exit(1); end"
|
|
46
|
+
end.join("; ")
|
|
47
|
+
|
|
48
|
+
# Build the script parts
|
|
49
|
+
script_parts = []
|
|
50
|
+
# Inject ARGV with --verbose for Minitest if profile is requested
|
|
51
|
+
# (Ruby's --verbose flag only sets $VERBOSE, doesn't enable Minitest verbose mode)
|
|
52
|
+
script_parts << "ARGV.replace(['--verbose'])" if options[:profile]
|
|
53
|
+
script_parts << requires_script
|
|
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)
|
|
65
|
+
elsif options[:profile]
|
|
66
|
+
# Single file without line number
|
|
67
|
+
cmd_parts << "-e"
|
|
68
|
+
escaped_path = files.gsub("'", "\\\\'")
|
|
69
|
+
path = files.start_with?("/", "./") ? escaped_path : "./#{escaped_path}"
|
|
70
|
+
cmd_parts << "\"ARGV.replace(['--verbose']); require '#{path}'; exit_code = Minitest.autorun; exit(exit_code)\""
|
|
71
|
+
# Inject ARGV with --verbose for Minitest profiling
|
|
72
|
+
else
|
|
73
|
+
# Just pass file as argument (Minitest autoruns)
|
|
74
|
+
cmd_parts << files
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Add any extra arguments
|
|
78
|
+
if options[:args]
|
|
79
|
+
cmd_parts.concat(Array(options[:args]))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
cmd_parts.join(" ")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def build_single_file_command(file, options = {})
|
|
86
|
+
# Single file uses the same logic as multiple files
|
|
87
|
+
build_test_command(file, options)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_pattern_command(pattern)
|
|
91
|
+
cmd = []
|
|
92
|
+
cmd << "bundle exec" if @bundler && bundler_available?
|
|
93
|
+
cmd << @ruby_command
|
|
94
|
+
cmd << "-Ilib:test"
|
|
95
|
+
cmd << "-e"
|
|
96
|
+
cmd << %{'Dir.glob("#{pattern}").each { |f| require f }'}
|
|
97
|
+
|
|
98
|
+
cmd.join(" ")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def build_line_number_command(cmd_parts, files, options = {})
|
|
104
|
+
# For files with line numbers, we need to:
|
|
105
|
+
# 1. Load each file
|
|
106
|
+
# 2. Resolve line numbers to test names
|
|
107
|
+
# 3. Filter using --name option
|
|
108
|
+
|
|
109
|
+
file_requires = []
|
|
110
|
+
test_names = []
|
|
111
|
+
|
|
112
|
+
files.each do |file_with_line|
|
|
113
|
+
parsed = LineNumberResolver.parse_file_with_line(file_with_line)
|
|
114
|
+
file_path = parsed[:file]
|
|
115
|
+
line_number = parsed[:line]
|
|
116
|
+
|
|
117
|
+
# Add ./ prefix if it's a relative path without one
|
|
118
|
+
path = file_path.start_with?("/", "./") ? file_path : "./#{file_path}"
|
|
119
|
+
escaped_path = path.gsub("'", "\\\\'")
|
|
120
|
+
|
|
121
|
+
# Always require the file
|
|
122
|
+
file_requires << "require '#{escaped_path}'"
|
|
123
|
+
|
|
124
|
+
# If there's a line number, resolve it to a test name
|
|
125
|
+
if line_number
|
|
126
|
+
test_name = LineNumberResolver.resolve_test_at_line(file_path, line_number)
|
|
127
|
+
if test_name
|
|
128
|
+
test_names << test_name
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Build the command
|
|
134
|
+
script_parts = []
|
|
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("; ")}\""
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def bundler_available?
|
|
160
|
+
@bundler_available ||= system("which bundle > /dev/null 2>&1")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module TestRunner
|
|
5
|
+
module Atoms
|
|
6
|
+
class LazyLoader
|
|
7
|
+
class << self
|
|
8
|
+
def load_formatter(format)
|
|
9
|
+
case format
|
|
10
|
+
when "json"
|
|
11
|
+
require_relative "../formatters/json_formatter"
|
|
12
|
+
Formatters::JsonFormatter
|
|
13
|
+
when "markdown"
|
|
14
|
+
require_relative "../formatters/markdown_formatter"
|
|
15
|
+
Formatters::MarkdownFormatter
|
|
16
|
+
when "progress"
|
|
17
|
+
require_relative "../formatters/progress_formatter"
|
|
18
|
+
Formatters::ProgressFormatter
|
|
19
|
+
when "progress-file"
|
|
20
|
+
require_relative "../formatters/progress_file_formatter"
|
|
21
|
+
Formatters::ProgressFileFormatter
|
|
22
|
+
else
|
|
23
|
+
raise ArgumentError, "Unknown format: #{format}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def load_molecule(name)
|
|
28
|
+
case name
|
|
29
|
+
when :pattern_resolver
|
|
30
|
+
require_relative "../molecules/pattern_resolver"
|
|
31
|
+
Molecules::PatternResolver
|
|
32
|
+
when :config_loader
|
|
33
|
+
require_relative "../molecules/config_loader"
|
|
34
|
+
Molecules::ConfigLoader
|
|
35
|
+
when :deprecation_fixer
|
|
36
|
+
require_relative "../molecules/deprecation_fixer"
|
|
37
|
+
Molecules::DeprecationFixer
|
|
38
|
+
when :rake_integration
|
|
39
|
+
require_relative "../molecules/rake_integration"
|
|
40
|
+
Molecules::RakeIntegration
|
|
41
|
+
else
|
|
42
|
+
raise ArgumentError, "Unknown molecule: #{name}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def load_organism(name)
|
|
47
|
+
case name
|
|
48
|
+
when :report_generator
|
|
49
|
+
require_relative "../organisms/report_generator"
|
|
50
|
+
Organisms::ReportGenerator
|
|
51
|
+
when :agent_reporter
|
|
52
|
+
require_relative "../organisms/agent_reporter"
|
|
53
|
+
Organisms::AgentReporter
|
|
54
|
+
else
|
|
55
|
+
raise ArgumentError, "Unknown organism: #{name}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module TestRunner
|
|
5
|
+
module Atoms
|
|
6
|
+
# Resolves line numbers to test method names
|
|
7
|
+
module LineNumberResolver
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Given a file and line number, find the test method name
|
|
11
|
+
# Returns the test name or nil if not found
|
|
12
|
+
def resolve_test_at_line(file_path, line_number)
|
|
13
|
+
return nil unless File.exist?(file_path)
|
|
14
|
+
|
|
15
|
+
content = File.read(file_path)
|
|
16
|
+
lines = content.split("\n")
|
|
17
|
+
|
|
18
|
+
# Find all test methods and their line ranges
|
|
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)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Extract test method names and their line ranges from file content
|
|
28
|
+
def extract_test_methods(lines)
|
|
29
|
+
test_methods = []
|
|
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
|
|
57
|
+
|
|
58
|
+
if current_indent <= 2 # Assuming test methods are indented at most 2 spaces
|
|
59
|
+
current_test[:end_line] = line_number
|
|
60
|
+
test_methods << current_test
|
|
61
|
+
current_test = nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Close last test if still open
|
|
67
|
+
if current_test
|
|
68
|
+
current_test[:end_line] = lines.size
|
|
69
|
+
test_methods << current_test
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
test_methods
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Given "file.rb:123", split into file and line number
|
|
76
|
+
def parse_file_with_line(file_with_line)
|
|
77
|
+
if file_with_line =~ /^(.+):(\d+)$/
|
|
78
|
+
{file: $1, line: $2.to_i}
|
|
79
|
+
else
|
|
80
|
+
{file: file_with_line, line: nil}
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/config"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module TestRunner
|
|
7
|
+
module Atoms
|
|
8
|
+
# Resolves canonical test report directories.
|
|
9
|
+
module ReportDirectoryResolver
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
DEFAULT_REPORT_ROOT = ".ace-local/test/reports"
|
|
13
|
+
|
|
14
|
+
def resolve_report_root(raw_report_dir, explicit_cli_override:, start_path:)
|
|
15
|
+
root = raw_report_dir.to_s.strip
|
|
16
|
+
root = DEFAULT_REPORT_ROOT if root.empty?
|
|
17
|
+
|
|
18
|
+
return File.expand_path(root, start_path) if explicit_cli_override
|
|
19
|
+
|
|
20
|
+
project_root = Ace::Support::Config.find_project_root(start_path: start_path)
|
|
21
|
+
base = project_root || start_path
|
|
22
|
+
File.expand_path(root, base)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def infer_package_name(package_dir:, test_files:, cwd:)
|
|
26
|
+
if package_dir && !package_dir.to_s.empty?
|
|
27
|
+
return File.basename(File.expand_path(package_dir))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
first = Array(test_files).first
|
|
31
|
+
if first && (match = first.match(%r{\A(.+?)/test/}))
|
|
32
|
+
return File.basename(match[1])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
File.basename(File.expand_path(cwd))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def short_package_name(package_name)
|
|
39
|
+
package_name.to_s.sub(/\Aace-/, "")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def resolve_package_report_dir(report_root:, package_name:)
|
|
43
|
+
File.join(report_root, short_package_name(package_name))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module TestRunner
|
|
5
|
+
module Atoms
|
|
6
|
+
module ReportPathResolver
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
REPORT_PRIORITY = [
|
|
10
|
+
"failures.json",
|
|
11
|
+
"summary.json",
|
|
12
|
+
"report.md",
|
|
13
|
+
"report.json",
|
|
14
|
+
"raw_output.txt"
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
# Resolves the best available report file path for a package
|
|
18
|
+
#
|
|
19
|
+
# The resolver checks for report files in the following priority order:
|
|
20
|
+
# 1. failures.json - Detailed failure information
|
|
21
|
+
# 2. summary.json - Summary of test results
|
|
22
|
+
# 3. report.md - Markdown formatted report
|
|
23
|
+
# 4. report.json - JSON formatted report
|
|
24
|
+
# 5. raw_output.txt - Raw test output
|
|
25
|
+
#
|
|
26
|
+
# @param package_path [String] The root path of the package
|
|
27
|
+
# @param report_root [String, nil] Centralized report root
|
|
28
|
+
# @param package_name [String, nil] Package name used for centralized lookup
|
|
29
|
+
# @return [String, nil] The absolute path to the best available report file, or nil if none exist
|
|
30
|
+
def call(package_path, report_root: nil, package_name: nil)
|
|
31
|
+
return nil unless package_path && Dir.exist?(package_path)
|
|
32
|
+
|
|
33
|
+
reports_dir = report_directory(package_path, report_root: report_root, package_name: package_name)
|
|
34
|
+
return nil unless reports_dir
|
|
35
|
+
|
|
36
|
+
REPORT_PRIORITY.each do |filename|
|
|
37
|
+
path = File.join(reports_dir, filename)
|
|
38
|
+
return path if File.exist?(path)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def report_directory(package_path, report_root: nil, package_name: nil)
|
|
45
|
+
candidates(package_path, report_root, package_name).each do |dir|
|
|
46
|
+
return dir if Dir.exist?(dir)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def candidates(package_path, report_root, package_name)
|
|
53
|
+
dirs = []
|
|
54
|
+
|
|
55
|
+
if report_root
|
|
56
|
+
short_name = package_name.to_s.sub(/\Aace-/, "")
|
|
57
|
+
short_name = File.basename(package_path).sub(/\Aace-/, "") if short_name.empty?
|
|
58
|
+
dirs << File.join(report_root, short_name, "latest")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
dirs << File.join(package_path, "test-reports", "latest")
|
|
62
|
+
dirs.uniq
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|