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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/test-runner/config.yml +2 -0
  3. data/CHANGELOG.md +27 -0
  4. data/exe/ace-test-suite +13 -4
  5. data/lib/ace/test_runner/atoms/command_builder.rb +60 -86
  6. data/lib/ace/test_runner/atoms/line_number_resolver.rb +70 -63
  7. data/lib/ace/test_runner/atoms/result_parser.rb +1 -1
  8. data/lib/ace/test_runner/cli/commands/test.rb +2 -3
  9. data/lib/ace/test_runner/formatters/json_formatter.rb +3 -1
  10. data/lib/ace/test_runner/formatters/progress_file_formatter.rb +10 -7
  11. data/lib/ace/test_runner/formatters/progress_formatter.rb +10 -7
  12. data/lib/ace/test_runner/models/test_report.rb +1 -0
  13. data/lib/ace/test_runner/models/test_result.rb +12 -3
  14. data/lib/ace/test_runner/models/test_selection_plan.rb +44 -0
  15. data/lib/ace/test_runner/molecules/cli_argument_parser.rb +9 -1
  16. data/lib/ace/test_runner/molecules/execution_evidence.rb +102 -0
  17. data/lib/ace/test_runner/molecules/failed_package_reporter.rb +4 -65
  18. data/lib/ace/test_runner/molecules/in_process_runner.rb +48 -37
  19. data/lib/ace/test_runner/molecules/pattern_resolver.rb +8 -5
  20. data/lib/ace/test_runner/molecules/report_storage.rb +24 -20
  21. data/lib/ace/test_runner/molecules/selection_resolver.rb +45 -0
  22. data/lib/ace/test_runner/molecules/selection_verifier.rb +78 -0
  23. data/lib/ace/test_runner/molecules/smart_test_executor.rb +4 -0
  24. data/lib/ace/test_runner/molecules/test_executor.rb +11 -4
  25. data/lib/ace/test_runner/organisms/sequential_target_executor.rb +6 -7
  26. data/lib/ace/test_runner/organisms/test_orchestrator.rb +44 -21
  27. data/lib/ace/test_runner/suite/display_manager.rb +7 -6
  28. data/lib/ace/test_runner/suite/orchestrator.rb +5 -3
  29. data/lib/ace/test_runner/suite/process_monitor.rb +37 -119
  30. data/lib/ace/test_runner/suite/result_aggregator.rb +17 -89
  31. data/lib/ace/test_runner/suite/simple_display_manager.rb +2 -1
  32. data/lib/ace/test_runner/version.rb +1 -1
  33. data/lib/ace/test_runner.rb +1 -0
  34. metadata +34 -2
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "test_executor"
4
+ require_relative "selection_resolver"
4
5
  require_relative "in_process_runner"
5
6
  require_relative "../atoms/test_type_detector"
6
7
 
@@ -20,6 +21,7 @@ module Ace
20
21
  return empty_result if files.empty?
21
22
 
22
23
  # Determine execution mode
24
+ options = options.merge(selection_plan: options[:selection_plan] || SelectionResolver.resolve(files))
23
25
  mode = determine_execution_mode(files, options)
24
26
 
25
27
  # Execute with appropriate runner
@@ -31,6 +33,7 @@ module Ace
31
33
  end
32
34
 
33
35
  def execute_single_file(file, options = {})
36
+ options = options.merge(selection_plan: options[:selection_plan] || SelectionResolver.resolve([file]))
34
37
  mode = determine_execution_mode([file], options)
35
38
 
36
39
  if mode == :direct
@@ -46,6 +49,7 @@ module Ace
46
49
  end
47
50
 
48
51
  def execute_with_progress(files, options = {}, &block)
52
+ options = options.merge(selection_plan: options[:selection_plan] || SelectionResolver.resolve(files))
49
53
  mode = determine_execution_mode(files, options)
50
54
 
51
55
  # Add mode information to progress callback
@@ -3,6 +3,7 @@
3
3
  require "open3"
4
4
  require "timeout"
5
5
  require "ostruct"
6
+ require_relative "selection_resolver"
6
7
 
7
8
  module Ace
8
9
  module TestRunner
@@ -39,15 +40,15 @@ module Ace
39
40
  # environment (Process.spawn merges onto the parent by default).
40
41
  # MT_NO_AUTORUN is part of the fixture environment.
41
42
  env = @launch_env
42
- command = command.sub(/^MT_NO_AUTORUN=1\s+/, "")
43
+ command = command.sub(/^MT_NO_AUTORUN=1\s+/, "") if command.is_a?(String)
43
44
 
44
45
  begin
45
46
  if @timeout
46
47
  Timeout.timeout(@timeout) do
47
- stdout, stderr, status = Open3.capture3(env, command, unsetenv_others: true)
48
+ stdout, stderr, status = Open3.capture3(env, *Array(command), unsetenv_others: true)
48
49
  end
49
50
  else
50
- stdout, stderr, status = Open3.capture3(env, command, unsetenv_others: true)
51
+ stdout, stderr, status = Open3.capture3(env, *Array(command), unsetenv_others: true)
51
52
  end
52
53
  rescue Timeout::Error
53
54
  stderr = "Test execution timed out after #{@timeout} seconds"
@@ -69,6 +70,7 @@ module Ace
69
70
  end
70
71
 
71
72
  def execute_with_progress(files, options = {}, &block)
73
+ options = options.merge(selection_plan: options[:selection_plan] || SelectionResolver.resolve(files))
72
74
  # Fail-fast requires per-file execution to stop on first failure
73
75
  # For performance, execute all files together unless explicitly disabled or fail-fast enabled
74
76
  if options[:per_file] == true || options[:fail_fast]
@@ -93,12 +95,16 @@ module Ace
93
95
  end
94
96
 
95
97
  def execute_per_file_with_progress(files, options = {}, &block)
98
+ options = options.merge(selection_plan: options[:selection_plan] || SelectionResolver.resolve(files))
96
99
  results = []
100
+ plan = options.fetch(:selection_plan)
101
+ files = plan.files if plan.qualified?
97
102
 
98
103
  files.each do |file|
99
104
  yield({type: :start, file: file}) if block_given?
100
105
 
101
- result = execute_single_file(file, options)
106
+ file_options = plan.qualified? ? options.merge(selection_plan: plan.for_file(file)) : options
107
+ result = execute_single_file(file, file_options)
102
108
  results << result
103
109
 
104
110
  if block_given?
@@ -139,6 +145,7 @@ module Ace
139
145
  stdout: results.map { |r| r[:stdout] }.join("\n"),
140
146
  stderr: results.map { |r| r[:stderr] }.join("\n"),
141
147
  commands: results.map { |r| r[:command] },
148
+ execution_outputs: results.map { |r| r[:stdout] },
142
149
  start_time: results.first[:start_time],
143
150
  end_time: results.last[:end_time],
144
151
  duration: results.sum { |r| r[:duration] },
@@ -37,7 +37,7 @@ module Ace
37
37
  # Parse result
38
38
  parsed = if result[:commands] && result[:commands].is_a?(Array)
39
39
  # Multiple commands executed (per-file)
40
- aggregate_results(result[:stdout], @result_parser)
40
+ aggregate_results(result.fetch(:execution_outputs), @result_parser)
41
41
  else
42
42
  @result_parser.parse_output(result[:stdout])
43
43
  end
@@ -81,10 +81,8 @@ module Ace
81
81
 
82
82
  private
83
83
 
84
- def aggregate_results(combined_output, parser)
85
- # Split output by test file executions
86
- individual_outputs = combined_output.split(/^Started with run options/)
87
- individual_outputs.shift if individual_outputs.first && individual_outputs.first.empty?
84
+ def aggregate_results(individual_outputs, parser)
85
+ # Preserve exact executor boundaries, including headerless partial output.
88
86
 
89
87
  aggregated = {
90
88
  summary: {
@@ -102,7 +100,6 @@ module Ace
102
100
  }
103
101
 
104
102
  individual_outputs.each do |output|
105
- output = "Started with run options" + output
106
103
  parsed = parser.parse_output(output)
107
104
 
108
105
  aggregated[:summary][:runs] += parsed[:summary][:runs]
@@ -160,7 +157,8 @@ module Ace
160
157
  combined_stderr = all_results.map { |r| r[:stderr] }.join("\n")
161
158
 
162
159
  # Determine success
163
- success = total_summary[:failures] == 0 && total_summary[:errors] == 0
160
+ success = all_results.all? { |result| result[:success] == true } &&
161
+ total_summary[:failures] == 0 && total_summary[:errors] == 0
164
162
 
165
163
  {
166
164
  stdout: combined_stdout,
@@ -175,6 +173,7 @@ module Ace
175
173
  duration: total_duration,
176
174
  test_times: all_test_times
177
175
  },
176
+ files: all_files.uniq,
178
177
  stopped_at_target: stopped
179
178
  }
180
179
  end
@@ -60,6 +60,7 @@ module Ace
60
60
  validate_configuration!
61
61
 
62
62
  start_time = Time.now
63
+ @execution_evidence = Molecules::ExecutionEvidence.for_invocation(Dir.pwd)
63
64
 
64
65
  # Print package context if running in different directory
65
66
  if @package_dir
@@ -82,6 +83,7 @@ module Ace
82
83
  end
83
84
 
84
85
  resolve_and_set_report_dir_context(test_files)
86
+ reserve_execution_report
85
87
 
86
88
  # Count total available test files (before filtering)
87
89
  total_available = count_total_test_files
@@ -104,22 +106,18 @@ module Ace
104
106
  runs: 0,
105
107
  assertions: 0,
106
108
  failures: 0,
107
- errors: test_files.size, # Count all test files as errors
109
+ errors: 0, # Execution failure is distinct from completed test errors
108
110
  skips: 0,
109
111
  passed: 0
110
112
  },
111
113
  failures: [],
112
- errors: [{
113
- message: execution_result[:stderr].strip,
114
- type: "LoadError",
115
- files: test_files
116
- }],
114
+ errors: [],
117
115
  deprecations: [],
118
116
  duration: execution_result[:duration]
119
117
  }
120
118
  elsif execution_result[:commands] && execution_result[:commands].is_a?(Array)
121
119
  # Each file was executed separately, parse and sum them all
122
- aggregate_individual_results(execution_result[:stdout])
120
+ aggregate_individual_results(execution_result.fetch(:execution_outputs))
123
121
  else
124
122
  # Single command execution (by-target)
125
123
  @result_parser.parse_output(execution_result[:stdout])
@@ -127,6 +125,7 @@ module Ace
127
125
 
128
126
  # Build result object
129
127
  @result = build_result(@parsed_result, execution_result, start_time)
128
+ validate_selected_execution
130
129
 
131
130
  # Analyze failures and errors
132
131
  if @result.has_failures?
@@ -165,6 +164,9 @@ module Ace
165
164
  @formatter.report_path = report_path if @formatter.respond_to?(:report_path=)
166
165
  end
167
166
 
167
+ # Publish completion only after all optional report files are written.
168
+ @execution_evidence.publish(@result, files: report.files_tested, report_dir: @execution_report_dir)
169
+
168
170
  # Output to stdout
169
171
  @formatter.on_finish(@result)
170
172
 
@@ -231,6 +233,7 @@ module Ace
231
233
  all_files = targets.flat_map { |t| t[:files] }
232
234
 
233
235
  resolve_and_set_report_dir_context(all_files)
236
+ reserve_execution_report
234
237
 
235
238
  total_available = count_total_test_files
236
239
 
@@ -262,11 +265,15 @@ module Ace
262
265
  end
263
266
  end
264
267
 
268
+ # Only dispatched targets belong in the execution report after an early stop.
269
+ all_files = execution_result.fetch(:files)
270
+
265
271
  # Use the parsed result from sequential executor
266
272
  @parsed_result = execution_result[:parsed_result]
267
273
 
268
274
  # Build result object
269
275
  @result = build_result(@parsed_result, execution_result, start_time)
276
+ validate_selected_execution
270
277
 
271
278
  # Analyze failures
272
279
  if @result.has_failures?
@@ -301,6 +308,9 @@ module Ace
301
308
  @formatter.report_path = report_path if @formatter.respond_to?(:report_path=)
302
309
  end
303
310
 
311
+ # Publish completion only after all optional report files are written.
312
+ @execution_evidence.publish(@result, files: report.files_tested, report_dir: @execution_report_dir)
313
+
304
314
  # Output to stdout
305
315
  @formatter.on_finish(@result)
306
316
 
@@ -463,6 +473,10 @@ module Ace
463
473
 
464
474
  def handle_no_tests
465
475
  @result = Models::TestResult.new
476
+ resolve_and_set_report_dir_context([])
477
+ reserve_execution_report
478
+ save_reports(@report_generator.generate(@result, [])) if @configuration.save_reports
479
+ @execution_evidence.publish(@result, files: [], report_dir: @execution_report_dir)
466
480
 
467
481
  message = if @configuration.filter
468
482
  "No test files found matching pattern '#{@configuration.filter}'"
@@ -471,6 +485,7 @@ module Ace
471
485
  end
472
486
 
473
487
  puts message
488
+ puts(@execution_report_dir ? "Details: #{@execution_report_dir}/" : "No saved report")
474
489
  0
475
490
  end
476
491
 
@@ -507,6 +522,13 @@ module Ace
507
522
  end
508
523
  end
509
524
 
525
+ def validate_selected_execution
526
+ return unless @result.success? && @result.total_tests == 0
527
+
528
+ @result.execution_success = false
529
+ @result.stderr = [@result.stderr, "Selected test files produced no executed tests"].reject(&:empty?).join("\n")
530
+ end
531
+
510
532
  def build_result(parsed_result, execution_result, start_time)
511
533
  Models::TestResult.new(
512
534
  passed: parsed_result[:summary][:passed],
@@ -519,17 +541,16 @@ module Ace
519
541
  end_time: Time.now,
520
542
  deprecations: parsed_result[:deprecations],
521
543
  raw_output: execution_result[:stdout],
522
- stderr: execution_result[:stderr]
544
+ stderr: execution_result[:stderr],
545
+ execution_success: execution_result[:success] == true
523
546
  )
524
547
  end
525
548
 
526
- def aggregate_individual_results(combined_output)
527
- # Split output by test file executions
528
- individual_outputs = combined_output.split(/^Started with run options/)
529
- individual_outputs.shift if individual_outputs.first && individual_outputs.first.empty?
549
+ def aggregate_individual_results(individual_outputs)
550
+ # Process boundaries come from the executor, never text inside stdout.
530
551
 
531
552
  aggregated = {
532
- raw_output: combined_output,
553
+ raw_output: individual_outputs.join("\n"),
533
554
  summary: {
534
555
  runs: 0,
535
556
  assertions: 0,
@@ -545,7 +566,6 @@ module Ace
545
566
  }
546
567
 
547
568
  individual_outputs.each do |output|
548
- output = "Started with run options" + output # Restore the split text
549
569
  parsed = @result_parser.parse_output(output)
550
570
 
551
571
  # Sum up the counts
@@ -602,19 +622,22 @@ module Ace
602
622
  )
603
623
  end
604
624
 
625
+ def reserve_execution_report
626
+ return unless @configuration.save_reports
627
+
628
+ @report_storage = Molecules::ReportStorage.new(base_dir: @configuration.report_dir)
629
+ @execution_report_dir = @report_storage.reserve(@execution_evidence.identity)
630
+ end
631
+
605
632
  def save_reports(report)
606
- timestamp_generator = Atoms::TimestampGenerator.new
607
- storage = Molecules::ReportStorage.new(
608
- base_dir: @configuration.report_dir,
609
- timestamp_generator: timestamp_generator
610
- )
633
+ storage = @report_storage
611
634
 
612
635
  # Save in appropriate format
613
636
  report_path = case @configuration.format
614
637
  when "json"
615
- storage.save_report(report, format: :json)
638
+ storage.save_report(report, format: :json, report_dir: @execution_report_dir)
616
639
  else
617
- storage.save_report(report, format: :all)
640
+ storage.save_report(report, format: :all, report_dir: @execution_report_dir)
618
641
  end
619
642
 
620
643
  # Always save raw output
@@ -33,9 +33,9 @@ module Ace
33
33
 
34
34
  # Reserve lines for each package
35
35
  @packages.each_with_index do |package, index|
36
- @lines[package["name"]] = index + 5 # Account for header lines
37
- @package_status[package["name"]] = {status: :waiting}
38
- print_package_line(package["name"])
36
+ @lines[package["entry_id"]] = index + 5 # Account for header lines
37
+ @package_status[package["entry_id"]] = {status: :waiting}
38
+ print_package_line(package["entry_id"])
39
39
  end
40
40
 
41
41
  # Print footer space
@@ -45,8 +45,8 @@ module Ace
45
45
  end
46
46
 
47
47
  def update_package(package, status, output = nil)
48
- @package_status[package["name"]] = status
49
- print_package_line(package["name"])
48
+ @package_status[package["entry_id"]] = status
49
+ print_package_line(package["entry_id"])
50
50
  update_footer
51
51
  end
52
52
 
@@ -87,7 +87,7 @@ module Ace
87
87
  print "\033[K" # Clear line
88
88
 
89
89
  # Format package name (fixed width, no brackets)
90
- pkg_name = name.ljust(25)
90
+ pkg_name = @packages.find { |package| package["entry_id"] == name }.fetch("name").ljust(25)
91
91
 
92
92
  case status[:status]
93
93
  when :waiting
@@ -135,6 +135,7 @@ module Ace
135
135
  line_text = "#{icon} #{elapsed} #{pkg_name} #{tests_col} #{asserts_col} #{fail_col}"
136
136
  line_text += " #{skipped} skip" if skipped > 0
137
137
 
138
+ line_text += results[:report_dir] ? " report: #{results[:report_dir]}" : " no saved report"
138
139
  print line_text
139
140
  end
140
141
  end
@@ -11,7 +11,9 @@ module Ace
11
11
 
12
12
  def initialize(config)
13
13
  @config = config
14
- @packages = config.dig("test_suite", "packages") || []
14
+ @packages = (config.dig("test_suite", "packages") || []).map do |package|
15
+ package.merge("entry_id" => SecureRandom.uuid)
16
+ end
15
17
  @results = {}
16
18
  @running_processes = {}
17
19
  @completed_packages = []
@@ -59,7 +61,7 @@ module Ace
59
61
 
60
62
  if status[:completed]
61
63
  @completed_packages << pkg
62
- @results[pkg["name"]] = status
64
+ @results[pkg["entry_id"]] = status
63
65
  end
64
66
  end
65
67
  end
@@ -75,7 +77,7 @@ module Ace
75
77
  display_manager.show_final_results
76
78
 
77
79
  # Aggregate results
78
- aggregator = ResultAggregator.new(@packages, report_root: report_root, runtime_results: @results)
80
+ aggregator = ResultAggregator.new(@packages, runtime_results: @results)
79
81
  summary = aggregator.aggregate
80
82
 
81
83
  display_manager.show_summary(summary)
@@ -30,6 +30,10 @@ module Ace
30
30
  return
31
31
  end
32
32
 
33
+ package["entry_id"] ||= SecureRandom.uuid
34
+ evidence = Molecules::ExecutionEvidence.new(package_path: package["path"],
35
+ package: package["name"], entry_id: package["entry_id"])
36
+
33
37
  # Build command
34
38
  cmd = build_command(package, test_options)
35
39
 
@@ -45,12 +49,16 @@ module Ace
45
49
  fixture_environment.env[Models::EnvironmentPolicy::SUITE_CHANNEL_KEY] =
46
50
  @environment_policy.channel_payload
47
51
 
52
+ fixture_environment.env[Molecules::ExecutionEvidence::CHANNEL] = evidence.channel
53
+
48
54
  start_time = now
49
55
  stdin, stdout, stderr, thread = Open3.popen3(
50
56
  fixture_environment.env, *cmd, chdir: package["path"], pgroup: true, unsetenv_others: true
51
57
  )
52
58
 
53
- @processes[package["name"]] = {
59
+ @processes[package["entry_id"]] = {
60
+ evidence: evidence,
61
+ save_reports: test_options.fetch("save_reports", true),
54
62
  package: package,
55
63
  thread: thread,
56
64
  stdout: stdout,
@@ -61,7 +69,6 @@ module Ace
61
69
  fixture_environment: fixture_environment,
62
70
  output: +"",
63
71
  stderr_output: +"",
64
- report_root: test_options["report_dir"],
65
72
  test_count: 0,
66
73
  tests_run: 0,
67
74
  dots: +"",
@@ -114,9 +121,8 @@ module Ace
114
121
 
115
122
  # Final callback
116
123
  if callback
117
- # Use results[:success] from summary.json if available, otherwise check exit code
118
- # This ensures the package status matches what ace-test actually reported
119
- success_status = (!results[:success].nil?) ? results[:success] : (exit_status == 0)
124
+ # Completion evidence and child outcome must both permit success.
125
+ success_status = results[:success] == true
120
126
 
121
127
  callback.call(package, {
122
128
  status: :completed,
@@ -174,11 +180,14 @@ module Ace
174
180
  rescue Timeout::Error, StandardError
175
181
  nil
176
182
  end
183
+ end
177
184
 
185
+ # Capture trustworthy partial counts and interruption outcome before cleanup.
186
+ check_processes
187
+ @processes.each_value do |process_info|
178
188
  close_streams(process_info)
179
189
  process_info[:fixture_environment]&.cleanup
180
190
  end
181
-
182
191
  @processes.clear
183
192
  @completed.clear
184
193
  end
@@ -213,7 +222,7 @@ module Ace
213
222
  cmd_parts << "--format" << format
214
223
 
215
224
  # Add other options
216
- cmd_parts << "--no-save" unless options["save_reports"]
225
+ cmd_parts << "--no-save-reports" unless options.fetch("save_reports", true)
217
226
  cmd_parts << "--fail-fast" if options["fail_fast"]
218
227
  cmd_parts << "--no-color" unless options.fetch("color", true)
219
228
  if options["report_dir"]
@@ -304,98 +313,31 @@ module Ace
304
313
  end
305
314
 
306
315
  def build_results(process_info, elapsed, exit_status)
307
- return timeout_results(process_info, elapsed) if process_info[:timeout_triggered]
308
- return interrupted_results(elapsed) if process_info[:terminated_by] == :interrupt
309
- fresh_summary = load_summary_results(process_info, elapsed)
310
- return fresh_summary if exit_status == 0 && fresh_summary
311
- return failed_process_results(process_info, elapsed, exit_status, fresh_summary) if exit_status != 0
312
-
313
- parse_results(process_info[:output])
314
- end
315
-
316
- def timeout_results(process_info, elapsed)
317
- {
318
- tests: 0,
319
- assertions: 0,
320
- failures: 0,
321
- errors: 1,
322
- duration: elapsed,
323
- success: false,
324
- error: "Timed out after #{process_info[:timeout]} seconds"
325
- }
326
- end
327
-
328
- def interrupted_results(elapsed)
329
- {
330
- tests: 0,
331
- assertions: 0,
332
- failures: 0,
333
- errors: 1,
334
- duration: elapsed,
335
- success: false,
336
- error: "Interrupted before completion"
337
- }
338
- end
339
-
340
- def failed_process_results(process_info, elapsed, exit_status, fresh_summary)
341
- return fresh_summary if fresh_summary && fresh_summary[:success] == false
342
-
343
- parsed = parse_results([process_info[:output], process_info[:stderr_output]].join("\n"))
344
- parsed[:duration] ||= elapsed
345
- parsed[:success] = false
346
- parsed[:error] ||= failure_message_for(process_info, exit_status)
347
- parsed[:errors] = 1 if parsed[:failures].to_i == 0 && parsed[:errors].to_i == 0
348
- parsed
316
+ snapshot = begin
317
+ process_info[:evidence].read(pid: process_info[:pid], save_reports: process_info[:save_reports])
318
+ rescue Ace::TestRunner::Error => e
319
+ {total: 0, passed: 0, failed: 0, errors: 1, skipped: 0, assertions: 0,
320
+ duration: elapsed, success: false, error: e.message}
321
+ end
322
+ results = snapshot.merge(tests: snapshot[:total], failures: snapshot[:failed],
323
+ execution_id: process_info[:evidence].identity, completion_path: process_info[:evidence].path)
324
+ reason = if process_info[:timeout_triggered]
325
+ "Timed out after #{process_info[:timeout]} seconds"
326
+ elsif process_info[:terminated_by] == :interrupt
327
+ "Interrupted before completion"
328
+ elsif exit_status != 0
329
+ failure_message_for(process_info, exit_status)
330
+ end
331
+ if reason
332
+ results[:success] = false
333
+ results[:error] = reason
334
+ end
335
+ results.freeze
349
336
  end
350
337
 
351
338
  def failure_message_for(process_info, exit_status)
352
339
  stderr = process_info[:stderr_output].to_s.strip
353
- stdout = process_info[:output].to_s.strip
354
- message = stderr.empty? ? stdout : stderr
355
- return message unless message.empty?
356
-
357
- "ace-test exited with status #{exit_status}"
358
- end
359
-
360
- def load_summary_results(process_info, elapsed)
361
- package = process_info[:package]
362
- reports_dir = Atoms::ReportPathResolver.report_directory(
363
- package["path"],
364
- report_root: process_info[:report_root],
365
- package_name: package["name"]
366
- )
367
- summary_file = reports_dir ? File.join(reports_dir, "summary.json") : nil
368
- return nil unless summary_file && File.exist?(summary_file)
369
- return nil unless summary_fresh_for_run?(summary_file, process_info[:start_time])
370
-
371
- json_data = JSON.parse(File.read(summary_file))
372
- results = {
373
- tests: json_data["total"] || 0,
374
- assertions: json_data["assertions"] || 0,
375
- failures: json_data["failed"] || 0,
376
- errors: json_data["errors"] || 0,
377
- skipped: json_data["skipped"] || 0,
378
- duration: json_data["duration"] || elapsed,
379
- success: json_data["success"] || false
380
- }
381
-
382
- if results[:assertions] == 0
383
- report_file = File.join(reports_dir, "report.json")
384
- if File.exist?(report_file)
385
- report_data = JSON.parse(File.read(report_file))
386
- results[:assertions] = report_data.dig("result", "assertions") || 0
387
- end
388
- end
389
-
390
- results
391
- rescue JSON::ParserError
392
- nil
393
- end
394
-
395
- def summary_fresh_for_run?(summary_file, start_time)
396
- File.mtime(summary_file) >= (start_time - 0.001)
397
- rescue StandardError
398
- false
340
+ stderr.empty? ? "ace-test exited with status #{exit_status.inspect}" : stderr
399
341
  end
400
342
 
401
343
  def parse_progress(process_info, chunk)
@@ -411,30 +353,6 @@ module Ace
411
353
  process_info[:test_count] = $1.to_i
412
354
  end
413
355
  end
414
-
415
- def parse_results(output)
416
- # Look for summary line in output
417
- if output =~ /(\d+) tests?, (\d+) assertions?, (\d+) failures?, (\d+) errors? \(([\d.]+)s\)/
418
- {
419
- tests: $1.to_i,
420
- assertions: $2.to_i,
421
- failures: $3.to_i,
422
- errors: $4.to_i,
423
- duration: $5.to_f,
424
- success: $3.to_i == 0 && $4.to_i == 0
425
- }
426
- else
427
- # Fallback to counting dots/F/E/S
428
- dots = output.scan(/[.FES]/).join
429
- {
430
- tests: dots.length,
431
- failures: dots.count("F"),
432
- errors: dots.count("E"),
433
- skipped: dots.count("S"),
434
- success: !dots.include?("F") && !dots.include?("E")
435
- }
436
- end
437
- end
438
356
  end
439
357
  end
440
358
  end