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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/test/runner.yml +35 -0
  3. data/.ace-defaults/test/suite.yml +31 -0
  4. data/.ace-defaults/test-runner/config.yml +61 -0
  5. data/CHANGELOG.md +626 -0
  6. data/LICENSE +21 -0
  7. data/README.md +42 -0
  8. data/Rakefile +14 -0
  9. data/exe/ace-test +26 -0
  10. data/exe/ace-test-suite +149 -0
  11. data/lib/ace/test_runner/atoms/command_builder.rb +165 -0
  12. data/lib/ace/test_runner/atoms/lazy_loader.rb +62 -0
  13. data/lib/ace/test_runner/atoms/line_number_resolver.rb +86 -0
  14. data/lib/ace/test_runner/atoms/report_directory_resolver.rb +48 -0
  15. data/lib/ace/test_runner/atoms/report_path_resolver.rb +67 -0
  16. data/lib/ace/test_runner/atoms/result_parser.rb +254 -0
  17. data/lib/ace/test_runner/atoms/test_detector.rb +114 -0
  18. data/lib/ace/test_runner/atoms/test_folder_detector.rb +53 -0
  19. data/lib/ace/test_runner/atoms/test_type_detector.rb +83 -0
  20. data/lib/ace/test_runner/atoms/timestamp_generator.rb +103 -0
  21. data/lib/ace/test_runner/cli/commands/test.rb +326 -0
  22. data/lib/ace/test_runner/cli.rb +16 -0
  23. data/lib/ace/test_runner/formatters/base_formatter.rb +102 -0
  24. data/lib/ace/test_runner/formatters/json_formatter.rb +90 -0
  25. data/lib/ace/test_runner/formatters/markdown_formatter.rb +91 -0
  26. data/lib/ace/test_runner/formatters/progress_file_formatter.rb +164 -0
  27. data/lib/ace/test_runner/formatters/progress_formatter.rb +328 -0
  28. data/lib/ace/test_runner/models/test_configuration.rb +165 -0
  29. data/lib/ace/test_runner/models/test_failure.rb +95 -0
  30. data/lib/ace/test_runner/models/test_group.rb +105 -0
  31. data/lib/ace/test_runner/models/test_report.rb +145 -0
  32. data/lib/ace/test_runner/models/test_result.rb +86 -0
  33. data/lib/ace/test_runner/molecules/cli_argument_parser.rb +263 -0
  34. data/lib/ace/test_runner/molecules/config_loader.rb +162 -0
  35. data/lib/ace/test_runner/molecules/deprecation_fixer.rb +204 -0
  36. data/lib/ace/test_runner/molecules/failed_package_reporter.rb +100 -0
  37. data/lib/ace/test_runner/molecules/failure_analyzer.rb +249 -0
  38. data/lib/ace/test_runner/molecules/in_process_runner.rb +249 -0
  39. data/lib/ace/test_runner/molecules/package_resolver.rb +106 -0
  40. data/lib/ace/test_runner/molecules/pattern_resolver.rb +146 -0
  41. data/lib/ace/test_runner/molecules/rake_integration.rb +218 -0
  42. data/lib/ace/test_runner/molecules/report_storage.rb +303 -0
  43. data/lib/ace/test_runner/molecules/smart_test_executor.rb +107 -0
  44. data/lib/ace/test_runner/molecules/test_executor.rb +162 -0
  45. data/lib/ace/test_runner/organisms/agent_reporter.rb +384 -0
  46. data/lib/ace/test_runner/organisms/report_generator.rb +151 -0
  47. data/lib/ace/test_runner/organisms/sequential_group_executor.rb +185 -0
  48. data/lib/ace/test_runner/organisms/test_orchestrator.rb +648 -0
  49. data/lib/ace/test_runner/rake_task.rb +90 -0
  50. data/lib/ace/test_runner/suite/display_helpers.rb +117 -0
  51. data/lib/ace/test_runner/suite/display_manager.rb +204 -0
  52. data/lib/ace/test_runner/suite/duration_estimator.rb +50 -0
  53. data/lib/ace/test_runner/suite/orchestrator.rb +120 -0
  54. data/lib/ace/test_runner/suite/process_monitor.rb +268 -0
  55. data/lib/ace/test_runner/suite/result_aggregator.rb +176 -0
  56. data/lib/ace/test_runner/suite/simple_display_manager.rb +122 -0
  57. data/lib/ace/test_runner/suite.rb +22 -0
  58. data/lib/ace/test_runner/version.rb +7 -0
  59. data/lib/ace/test_runner.rb +69 -0
  60. 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