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
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require_relative "../../version"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module TestRunner
|
|
8
|
+
module CLI
|
|
9
|
+
module Commands
|
|
10
|
+
# ace-support-cli Command class for the test command
|
|
11
|
+
#
|
|
12
|
+
# This command runs tests with flexible package, target, and file selection.
|
|
13
|
+
# All business logic is inline in this single command class.
|
|
14
|
+
class Test < Ace::Support::Cli::Command
|
|
15
|
+
include Ace::Support::Cli::Base
|
|
16
|
+
|
|
17
|
+
desc <<~DESC.strip
|
|
18
|
+
Run tests with flexible package, target, and file selection
|
|
19
|
+
|
|
20
|
+
SYNTAX:
|
|
21
|
+
ace-test [PACKAGE] [TARGET] [options] [files...]
|
|
22
|
+
|
|
23
|
+
PACKAGE (optional):
|
|
24
|
+
ace-* Run tests in specified package (e.g., ace-bundle, ace-nav)
|
|
25
|
+
./path Run tests in package at relative path
|
|
26
|
+
/path Run tests in package at absolute path
|
|
27
|
+
|
|
28
|
+
TARGETS:
|
|
29
|
+
atoms Run atom tests only
|
|
30
|
+
molecules Run molecule tests only
|
|
31
|
+
organisms Run organism tests only
|
|
32
|
+
models Run model tests only
|
|
33
|
+
unit Run all unit tests (atoms + molecules + organisms + models)
|
|
34
|
+
integration Run integration tests
|
|
35
|
+
system Run system tests
|
|
36
|
+
all Run all tests (default)
|
|
37
|
+
quick Run quick tests (atoms + molecules)
|
|
38
|
+
|
|
39
|
+
CONFIGURATION:
|
|
40
|
+
Global config: ~/.ace/test-runner/config.yml
|
|
41
|
+
Project config: .ace/test-runner/config.yml
|
|
42
|
+
Example: ace-test-runner/.ace-defaults/test-runner/config.yml
|
|
43
|
+
|
|
44
|
+
Can also use .ace/test-runner.yml for project-level config
|
|
45
|
+
|
|
46
|
+
OUTPUT:
|
|
47
|
+
By default, shows progress bar with summary
|
|
48
|
+
Use --format json for structured output
|
|
49
|
+
Reports saved to .ace-local/test/reports/<package>/ by default
|
|
50
|
+
Exit codes: 0 (pass), 1 (fail), 2 (error)
|
|
51
|
+
|
|
52
|
+
TEST HIERARCHY:
|
|
53
|
+
ATOM Architecture Test Layers:
|
|
54
|
+
- atoms -> Pure functions (no side effects)
|
|
55
|
+
- molecules -> Composed operations (controlled side effects)
|
|
56
|
+
- organisms -> Business logic (complex coordination)
|
|
57
|
+
- models -> Data structures (no behavior)
|
|
58
|
+
- integration -> Cross-component testing
|
|
59
|
+
- system -> End-to-end workflows
|
|
60
|
+
DESC
|
|
61
|
+
|
|
62
|
+
# Examples shown in help output
|
|
63
|
+
example [
|
|
64
|
+
" # All tests in current package",
|
|
65
|
+
"atoms # Atom tests only",
|
|
66
|
+
"ace-bundle molecules # Package + target",
|
|
67
|
+
"test/foo_test.rb # Specific test file",
|
|
68
|
+
"--format progress # Run with progress output",
|
|
69
|
+
"--profile 10 # Profile slowest tests",
|
|
70
|
+
"--fail-fast # Stop on first failure"
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
# Output format options
|
|
74
|
+
option :format, type: :string, aliases: %w[-f], desc: "Output format: progress (default), progress-file, json"
|
|
75
|
+
option :report_dir, type: :string, aliases: %w[-d], desc: "Report root directory (default: .ace-local/test/reports)"
|
|
76
|
+
option :save_reports, type: :boolean, desc: "Skip saving detailed reports"
|
|
77
|
+
|
|
78
|
+
# Test execution options
|
|
79
|
+
option :fail_fast, type: :boolean, desc: "Stop execution on first failure"
|
|
80
|
+
option :fix_deprecations, type: :boolean, desc: "Auto-fix deprecated test patterns"
|
|
81
|
+
option :filter, type: :string, desc: "Run only tests matching pattern"
|
|
82
|
+
option :group, type: :string, aliases: %w[-g], desc: "Run specific test group (unit, integration, system, all)"
|
|
83
|
+
option :color, type: :boolean, desc: "Enable/disable colored output (default: enabled)"
|
|
84
|
+
option :config_path, type: :string, aliases: %w[-c], desc: "Configuration file path (default: .ace/test-runner.yml)"
|
|
85
|
+
|
|
86
|
+
# Type conversion options (ace-support-cli returns strings, need to convert to integers)
|
|
87
|
+
option :timeout, type: :string, desc: "Timeout for test execution in seconds"
|
|
88
|
+
option :max_display, type: :string, desc: "Maximum failures to display (default: 7)"
|
|
89
|
+
option :profile, type: :string, desc: "Show N slowest tests (default: 10)"
|
|
90
|
+
|
|
91
|
+
# Execution mode options
|
|
92
|
+
option :parallel, type: :boolean, desc: "Run tests in parallel (experimental)"
|
|
93
|
+
option :per_file, type: :boolean, desc: "Execute each test file separately (slower, for debugging)"
|
|
94
|
+
option :direct, type: :boolean, desc: "Force in-process execution (faster, less isolation)"
|
|
95
|
+
option :subprocess, type: :boolean, desc: "Force subprocess execution (slower, full isolation)"
|
|
96
|
+
option :run_in_sequence, type: :boolean, aliases: %w[--ris], desc: "Run test groups sequentially (default)"
|
|
97
|
+
option :run_in_single_batch, type: :boolean, aliases: %w[--risb], desc: "Run all tests together in single batch"
|
|
98
|
+
|
|
99
|
+
# Rake integration options
|
|
100
|
+
option :set_default_rake, type: :boolean, desc: "Set ace-test as default rake test runner"
|
|
101
|
+
option :unset_default_rake, type: :boolean, desc: "Remove ace-test as default rake test runner"
|
|
102
|
+
option :check_rake_status, type: :boolean, desc: "Check if ace-test is set as default rake test runner"
|
|
103
|
+
|
|
104
|
+
# Cleanup options
|
|
105
|
+
option :cleanup_reports, type: :boolean, desc: "Clean up old test reports (keeps last 10)"
|
|
106
|
+
option :cleanup_keep, type: :string, desc: "Number of reports to keep when cleaning (default: 10)"
|
|
107
|
+
option :cleanup_age, type: :string, desc: "Delete reports older than DAYS (default: 30)"
|
|
108
|
+
|
|
109
|
+
# Standard options (inherited from Base but need explicit definition for ace-support-cli)
|
|
110
|
+
option :version, type: :boolean, desc: "Show version information"
|
|
111
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
112
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
113
|
+
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
114
|
+
|
|
115
|
+
def call(args: [], **options)
|
|
116
|
+
if options[:version]
|
|
117
|
+
puts "ace-test #{Ace::TestRunner::VERSION}"
|
|
118
|
+
return 0
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Type-convert numeric options (ace-support-cli returns strings, Thor converted to integers)
|
|
122
|
+
# This maintains parity with the Thor implementation
|
|
123
|
+
numeric_options = %i[timeout max_display profile cleanup_keep cleanup_age]
|
|
124
|
+
numeric_options.each do |key|
|
|
125
|
+
options[key] = options[key].to_i if options[key]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Build test options with defaults
|
|
129
|
+
test_options = build_test_options(options)
|
|
130
|
+
|
|
131
|
+
# Display config summary
|
|
132
|
+
display_config_summary(test_options)
|
|
133
|
+
|
|
134
|
+
# Handle special modes first
|
|
135
|
+
result = handle_special_modes(args, test_options)
|
|
136
|
+
return result if result
|
|
137
|
+
|
|
138
|
+
# Parse CLI arguments using CliArgumentParser
|
|
139
|
+
begin
|
|
140
|
+
arg_parser = Molecules::CliArgumentParser.new(args)
|
|
141
|
+
parsed_args = arg_parser.parse
|
|
142
|
+
test_options.merge!(parsed_args)
|
|
143
|
+
rescue ArgumentError => e
|
|
144
|
+
raise Ace::Support::Cli::Error.new(e.message)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Run tests with special exit! handling for Minitest compatibility
|
|
148
|
+
run_tests_with_exit_handling(test_options)
|
|
149
|
+
rescue Ace::TestRunner::Error => e
|
|
150
|
+
raise Ace::Support::Cli::Error.new(e.message)
|
|
151
|
+
rescue Interrupt
|
|
152
|
+
raise Ace::Support::Cli::Error.new("Test execution interrupted", exit_code: 130)
|
|
153
|
+
rescue => e
|
|
154
|
+
raise Ace::Support::Cli::Error.new("Unexpected error: #{e.message}")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def build_test_options(cli_options)
|
|
160
|
+
# Thor returns HashWithIndifferentAccess with string keys internally.
|
|
161
|
+
# Transform to symbol keys for consistent access patterns throughout the codebase.
|
|
162
|
+
symbolized_options = cli_options.transform_keys(&:to_sym)
|
|
163
|
+
|
|
164
|
+
# Handle mutually exclusive run mode options
|
|
165
|
+
# --run-in-sequence sets run_in_single_batch to false (default behavior)
|
|
166
|
+
# --run-in-single-batch sets run_in_single_batch to true
|
|
167
|
+
if symbolized_options[:run_in_sequence]
|
|
168
|
+
symbolized_options[:run_in_single_batch] = false
|
|
169
|
+
end
|
|
170
|
+
# Remove the run_in_sequence key as it's only used to set run_in_single_batch
|
|
171
|
+
symbolized_options.delete(:run_in_sequence)
|
|
172
|
+
|
|
173
|
+
# Default options - symbolized_options will override these for any explicit values
|
|
174
|
+
{
|
|
175
|
+
format: "progress",
|
|
176
|
+
save_reports: true,
|
|
177
|
+
verbose: false,
|
|
178
|
+
fail_fast: false,
|
|
179
|
+
fix_deprecations: false,
|
|
180
|
+
color: true,
|
|
181
|
+
per_file: false,
|
|
182
|
+
run_in_single_batch: false
|
|
183
|
+
}.merge(symbolized_options)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def handle_special_modes(args, options)
|
|
187
|
+
# Handle cleanup reports mode
|
|
188
|
+
if options[:cleanup_reports]
|
|
189
|
+
return handle_cleanup_reports(options)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Handle rake integration modes
|
|
193
|
+
if options[:set_default_rake] || options[:unset_default_rake] || options[:check_rake_status]
|
|
194
|
+
return handle_rake_integration(options)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Handle deprecation fixing mode
|
|
198
|
+
if options[:fix_deprecations]
|
|
199
|
+
return handle_fix_deprecations
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
nil # Continue to normal test execution
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def handle_cleanup_reports(options)
|
|
206
|
+
require "ace/test_runner/molecules/report_storage"
|
|
207
|
+
require "ace/test_runner/atoms/timestamp_generator"
|
|
208
|
+
require "ace/test_runner/molecules/config_loader"
|
|
209
|
+
|
|
210
|
+
# Load config from cascade (ADR-022 pattern)
|
|
211
|
+
config_loader = Molecules::ConfigLoader.new
|
|
212
|
+
config_data = config_loader.load(options[:config_path])
|
|
213
|
+
config = config_loader.merge_with_options(config_data, options)
|
|
214
|
+
|
|
215
|
+
timestamp_generator = Atoms::TimestampGenerator.new
|
|
216
|
+
storage = Molecules::ReportStorage.new(
|
|
217
|
+
base_dir: config[:defaults][:report_dir] || ".ace-local/test/reports",
|
|
218
|
+
timestamp_generator: timestamp_generator
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
keep = options[:cleanup_keep] || 10
|
|
222
|
+
age = options[:cleanup_age] || 30
|
|
223
|
+
|
|
224
|
+
puts "Cleaning up test reports..."
|
|
225
|
+
puts " Keeping last #{keep} reports"
|
|
226
|
+
puts " Deleting reports older than #{age} days"
|
|
227
|
+
|
|
228
|
+
deleted = storage.cleanup_old_reports(keep: keep, max_age_days: age)
|
|
229
|
+
|
|
230
|
+
if deleted.empty?
|
|
231
|
+
puts "No reports to clean up."
|
|
232
|
+
else
|
|
233
|
+
puts "Deleted #{deleted.size} old reports:"
|
|
234
|
+
deleted.each { |path| puts " - #{File.basename(path)}" }
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def handle_rake_integration(options)
|
|
239
|
+
require "ace/test_runner/molecules/rake_integration"
|
|
240
|
+
|
|
241
|
+
integration = Molecules::RakeIntegration.new
|
|
242
|
+
|
|
243
|
+
if options[:set_default_rake]
|
|
244
|
+
result = integration.set_default
|
|
245
|
+
puts result[:message]
|
|
246
|
+
raise Ace::Support::Cli::Error.new(result[:message]) unless result[:success]
|
|
247
|
+
nil
|
|
248
|
+
elsif options[:unset_default_rake]
|
|
249
|
+
result = integration.unset_default
|
|
250
|
+
puts result[:message]
|
|
251
|
+
raise Ace::Support::Cli::Error.new(result[:message]) unless result[:success]
|
|
252
|
+
nil
|
|
253
|
+
elsif options[:check_rake_status]
|
|
254
|
+
status = integration.check_status
|
|
255
|
+
puts "Rake Test Integration Status:"
|
|
256
|
+
puts " Rakefile exists: #{status[:rakefile_exists]}"
|
|
257
|
+
puts " ace-test integrated: #{status[:integrated]}"
|
|
258
|
+
puts " Message: #{status[:message]}"
|
|
259
|
+
puts " Backup exists: #{status[:backup_exists]}" if status[:backup_exists]
|
|
260
|
+
puts " Has test task: #{status[:has_test_task]}" if status.key?(:has_test_task)
|
|
261
|
+
nil
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def handle_fix_deprecations
|
|
266
|
+
require "ace/test_runner/molecules/deprecation_fixer"
|
|
267
|
+
|
|
268
|
+
puts "Scanning for deprecated test patterns..."
|
|
269
|
+
|
|
270
|
+
detector = Atoms::TestDetector.new
|
|
271
|
+
fixer = Molecules::DeprecationFixer.new
|
|
272
|
+
|
|
273
|
+
test_files = detector.find_test_files
|
|
274
|
+
fixed_count = 0
|
|
275
|
+
|
|
276
|
+
test_files.each do |file|
|
|
277
|
+
result = fixer.fix_file(file, dry_run: false)
|
|
278
|
+
if result[:success] && result[:changes] > 0
|
|
279
|
+
puts " Fixed #{result[:changes]} deprecations in #{file}"
|
|
280
|
+
fixed_count += result[:changes]
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
if fixed_count == 0
|
|
285
|
+
puts "No deprecations found to fix."
|
|
286
|
+
else
|
|
287
|
+
puts "Fixed #{fixed_count} deprecations total."
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def run_tests_with_exit_handling(options)
|
|
292
|
+
exit_code = Ace::TestRunner.run(options)
|
|
293
|
+
|
|
294
|
+
# Flush IO buffers before exit! (which skips at_exit handlers including finalizers)
|
|
295
|
+
$stdout.flush
|
|
296
|
+
$stderr.flush
|
|
297
|
+
|
|
298
|
+
# IMPORTANT: Use exit! (not exit) to skip Ruby's at_exit handlers.
|
|
299
|
+
# This prevents Minitest from auto-running again via its at_exit hook.
|
|
300
|
+
# When ace-test uses in-process (direct) execution, test files are loaded
|
|
301
|
+
# into the current process. Minitest registers an at_exit handler that would
|
|
302
|
+
# re-run all tests on normal exit. Using exit! bypasses this.
|
|
303
|
+
# See guide://testable-code-patterns for details on this pattern.
|
|
304
|
+
|
|
305
|
+
# Note: We cannot use exit! here and return the exit code instead.
|
|
306
|
+
# The exe file wrapper will handle the exit! call.
|
|
307
|
+
exit_code
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def display_config_summary(options)
|
|
311
|
+
return if options[:quiet]
|
|
312
|
+
|
|
313
|
+
require "ace/core"
|
|
314
|
+
Ace::Core::Atoms::ConfigSummary.display(
|
|
315
|
+
command: "test",
|
|
316
|
+
config: options,
|
|
317
|
+
defaults: {},
|
|
318
|
+
options: options,
|
|
319
|
+
quiet: false
|
|
320
|
+
)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require "ace/core"
|
|
5
|
+
require_relative "version"
|
|
6
|
+
# Commands
|
|
7
|
+
require_relative "cli/commands/test"
|
|
8
|
+
|
|
9
|
+
module Ace
|
|
10
|
+
module TestRunner
|
|
11
|
+
# CLI namespace for ace-test-runner command classes.
|
|
12
|
+
module CLI
|
|
13
|
+
PROGRAM_NAME = "ace-test"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module TestRunner
|
|
5
|
+
module Formatters
|
|
6
|
+
# Base class for all formatters
|
|
7
|
+
class BaseFormatter
|
|
8
|
+
attr_reader :options
|
|
9
|
+
attr_accessor :report_path
|
|
10
|
+
|
|
11
|
+
def initialize(options = {})
|
|
12
|
+
@options = options
|
|
13
|
+
@use_color = options.fetch(:color, true) && $stdout.tty?
|
|
14
|
+
@report_path = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Format the test result for stdout output
|
|
18
|
+
def format_stdout(result)
|
|
19
|
+
raise NotImplementedError, "Subclasses must implement format_stdout"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Format the complete report for saving
|
|
23
|
+
def format_report(report)
|
|
24
|
+
raise NotImplementedError, "Subclasses must implement format_report"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Called when test execution starts
|
|
28
|
+
def on_start(total_files)
|
|
29
|
+
# Override in subclasses if needed
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Called when a single test file completes
|
|
33
|
+
def on_test_complete(file, success, duration)
|
|
34
|
+
# Override in subclasses if needed
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Called when all tests complete
|
|
38
|
+
def on_finish(result)
|
|
39
|
+
# Override in subclasses if needed
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
protected
|
|
43
|
+
|
|
44
|
+
def colorize(text, color)
|
|
45
|
+
return text unless @use_color
|
|
46
|
+
|
|
47
|
+
color_codes = {
|
|
48
|
+
red: "\e[31m",
|
|
49
|
+
green: "\e[32m",
|
|
50
|
+
yellow: "\e[33m",
|
|
51
|
+
blue: "\e[34m",
|
|
52
|
+
magenta: "\e[35m",
|
|
53
|
+
cyan: "\e[36m",
|
|
54
|
+
white: "\e[37m",
|
|
55
|
+
bold: "\e[1m",
|
|
56
|
+
reset: "\e[0m"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
code = color_codes[color] || color_codes[:reset]
|
|
60
|
+
"#{code}#{text}#{color_codes[:reset]}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def pluralize(count, singular, plural = nil)
|
|
64
|
+
plural ||= "#{singular}s"
|
|
65
|
+
(count == 1) ? "#{count} #{singular}" : "#{count} #{plural}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def format_duration(seconds)
|
|
69
|
+
if seconds < 1
|
|
70
|
+
"#{(seconds * 1000).round(2)}ms"
|
|
71
|
+
elsif seconds < 60
|
|
72
|
+
"#{seconds.round(2)}s"
|
|
73
|
+
else
|
|
74
|
+
minutes = (seconds / 60).floor
|
|
75
|
+
remaining = (seconds % 60).round
|
|
76
|
+
"#{minutes}m #{remaining}s"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def success_icon
|
|
81
|
+
"✅"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def failure_icon
|
|
85
|
+
"❌"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def error_icon
|
|
89
|
+
"💥"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def skip_icon
|
|
93
|
+
"⚠️"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def format_percentage(value)
|
|
97
|
+
"#{value.round(1)}%"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module TestRunner
|
|
5
|
+
module Formatters
|
|
6
|
+
# JSON formatter for machine-readable output
|
|
7
|
+
class JsonFormatter < BaseFormatter
|
|
8
|
+
def format_stdout(result)
|
|
9
|
+
# For JSON format, stdout gets the complete JSON
|
|
10
|
+
JSON.pretty_generate(format_result_hash(result))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def format_report(report)
|
|
14
|
+
# Complete report in JSON format
|
|
15
|
+
{
|
|
16
|
+
version: "1.0",
|
|
17
|
+
generator: "ace-test-runner",
|
|
18
|
+
timestamp: report.timestamp.iso8601,
|
|
19
|
+
environment: report.environment,
|
|
20
|
+
configuration: report.configuration.to_h,
|
|
21
|
+
summary: report.summary,
|
|
22
|
+
result: format_result_hash(report.result),
|
|
23
|
+
failures: format_failures_array(report.result.failures_detail),
|
|
24
|
+
deprecations: report.result.deprecations,
|
|
25
|
+
files_tested: report.files_tested,
|
|
26
|
+
metadata: report.metadata
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def on_start(total_files)
|
|
31
|
+
# JSON formatter doesn't output progress
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def on_test_complete(file, success, duration)
|
|
35
|
+
# JSON formatter doesn't output progress
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def on_finish(result)
|
|
39
|
+
# Output complete JSON at the end
|
|
40
|
+
puts format_stdout(result)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def format_result_hash(result)
|
|
46
|
+
{
|
|
47
|
+
summary: {
|
|
48
|
+
passed: result.passed,
|
|
49
|
+
failed: result.failed,
|
|
50
|
+
errors: result.errors,
|
|
51
|
+
skipped: result.skipped,
|
|
52
|
+
total: result.total_tests,
|
|
53
|
+
assertions: result.assertions,
|
|
54
|
+
duration: result.duration,
|
|
55
|
+
pass_rate: result.pass_rate,
|
|
56
|
+
success: result.success?
|
|
57
|
+
},
|
|
58
|
+
timing: {
|
|
59
|
+
start_time: result.start_time&.iso8601,
|
|
60
|
+
end_time: result.end_time&.iso8601,
|
|
61
|
+
duration_seconds: result.duration,
|
|
62
|
+
duration_formatted: format_duration(result.duration)
|
|
63
|
+
},
|
|
64
|
+
failures: format_failures_array(result.failures_detail),
|
|
65
|
+
deprecations: result.deprecations
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def format_failures_array(failures)
|
|
70
|
+
failures.map do |failure|
|
|
71
|
+
{
|
|
72
|
+
type: failure.type.to_s,
|
|
73
|
+
test_name: failure.test_name,
|
|
74
|
+
test_class: failure.test_class,
|
|
75
|
+
full_name: failure.full_test_name,
|
|
76
|
+
message: failure.message,
|
|
77
|
+
location: {
|
|
78
|
+
file: failure.file_path,
|
|
79
|
+
line: failure.line_number,
|
|
80
|
+
full: failure.location
|
|
81
|
+
},
|
|
82
|
+
fix_suggestion: failure.fix_suggestion,
|
|
83
|
+
backtrace: failure.backtrace
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_formatter"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module TestRunner
|
|
7
|
+
module Formatters
|
|
8
|
+
# Markdown formatter for generating individual failure report files
|
|
9
|
+
# This formatter is used internally for creating detailed failure reports
|
|
10
|
+
# It is not exposed as a user-selectable output format
|
|
11
|
+
class MarkdownFormatter < BaseFormatter
|
|
12
|
+
def initialize(options = {})
|
|
13
|
+
super
|
|
14
|
+
@configuration = options
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Generate markdown report for a single failure
|
|
18
|
+
def generate_failure_report(failure, index)
|
|
19
|
+
lines = []
|
|
20
|
+
|
|
21
|
+
# Title with status
|
|
22
|
+
status = failure.error? ? "ERROR" : "FAILURE"
|
|
23
|
+
lines << "# Test #{status}: #{failure.test_name}"
|
|
24
|
+
lines << ""
|
|
25
|
+
lines << "**Status:** #{status}"
|
|
26
|
+
lines << "**Location:** #{failure.location}" if failure.location
|
|
27
|
+
lines << ""
|
|
28
|
+
|
|
29
|
+
# Error Message
|
|
30
|
+
lines << "## Error Message"
|
|
31
|
+
lines << ""
|
|
32
|
+
lines << failure.message if failure.message
|
|
33
|
+
lines << ""
|
|
34
|
+
|
|
35
|
+
# Stack Trace
|
|
36
|
+
if failure.backtrace && !failure.backtrace.empty?
|
|
37
|
+
lines << "## Stack Trace"
|
|
38
|
+
lines << ""
|
|
39
|
+
lines << "```"
|
|
40
|
+
lines << failure.backtrace.take(15).join("\n")
|
|
41
|
+
lines << "```"
|
|
42
|
+
lines << ""
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Related stderr
|
|
46
|
+
if failure.stderr_warnings && !failure.stderr_warnings.empty?
|
|
47
|
+
lines << "## Related stderr"
|
|
48
|
+
lines << ""
|
|
49
|
+
lines << "```"
|
|
50
|
+
lines << failure.stderr_warnings
|
|
51
|
+
lines << "```"
|
|
52
|
+
lines << ""
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Code Context
|
|
56
|
+
if failure.code_context && failure.code_context[:lines]
|
|
57
|
+
lines << "## Code Context"
|
|
58
|
+
lines << ""
|
|
59
|
+
lines << "```ruby"
|
|
60
|
+
failure.code_context[:lines].each do |line_num, line_data|
|
|
61
|
+
marker = line_data[:highlighted] ? "← ERROR HERE" : ""
|
|
62
|
+
lines << format("%3d: %s %s", line_num, line_data[:content], marker)
|
|
63
|
+
end
|
|
64
|
+
lines << "```"
|
|
65
|
+
lines << ""
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Fix Suggestion
|
|
69
|
+
if failure.fix_suggestion
|
|
70
|
+
lines << "## Fix Suggestion"
|
|
71
|
+
lines << ""
|
|
72
|
+
lines << failure.fix_suggestion
|
|
73
|
+
lines << ""
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
lines.join("\n")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Not used for console output
|
|
80
|
+
def format_stdout(result)
|
|
81
|
+
""
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Not used for report generation
|
|
85
|
+
def format_report(report)
|
|
86
|
+
{}
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|