ace-test-runner 0.27.1 → 0.28.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 +4 -4
- data/CHANGELOG.md +14 -0
- data/exe/ace-test-suite +13 -4
- data/lib/ace/test_runner/cli/commands/test.rb +1 -3
- data/lib/ace/test_runner/formatters/json_formatter.rb +3 -1
- data/lib/ace/test_runner/formatters/progress_file_formatter.rb +10 -7
- data/lib/ace/test_runner/formatters/progress_formatter.rb +10 -7
- data/lib/ace/test_runner/models/test_report.rb +1 -0
- data/lib/ace/test_runner/models/test_result.rb +12 -3
- data/lib/ace/test_runner/molecules/execution_evidence.rb +102 -0
- data/lib/ace/test_runner/molecules/failed_package_reporter.rb +4 -65
- data/lib/ace/test_runner/molecules/in_process_runner.rb +27 -5
- data/lib/ace/test_runner/molecules/report_storage.rb +24 -20
- data/lib/ace/test_runner/molecules/test_executor.rb +1 -0
- data/lib/ace/test_runner/organisms/sequential_target_executor.rb +5 -7
- data/lib/ace/test_runner/organisms/test_orchestrator.rb +41 -21
- data/lib/ace/test_runner/suite/display_manager.rb +7 -6
- data/lib/ace/test_runner/suite/orchestrator.rb +5 -3
- data/lib/ace/test_runner/suite/process_monitor.rb +37 -119
- data/lib/ace/test_runner/suite/result_aggregator.rb +17 -89
- data/lib/ace/test_runner/suite/simple_display_manager.rb +2 -1
- data/lib/ace/test_runner/version.rb +1 -1
- data/lib/ace/test_runner.rb +1 -0
- metadata +17 -2
|
@@ -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:
|
|
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
|
|
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
|
|
|
@@ -267,6 +270,7 @@ module Ace
|
|
|
267
270
|
|
|
268
271
|
# Build result object
|
|
269
272
|
@result = build_result(@parsed_result, execution_result, start_time)
|
|
273
|
+
validate_selected_execution
|
|
270
274
|
|
|
271
275
|
# Analyze failures
|
|
272
276
|
if @result.has_failures?
|
|
@@ -301,6 +305,9 @@ module Ace
|
|
|
301
305
|
@formatter.report_path = report_path if @formatter.respond_to?(:report_path=)
|
|
302
306
|
end
|
|
303
307
|
|
|
308
|
+
# Publish completion only after all optional report files are written.
|
|
309
|
+
@execution_evidence.publish(@result, files: report.files_tested, report_dir: @execution_report_dir)
|
|
310
|
+
|
|
304
311
|
# Output to stdout
|
|
305
312
|
@formatter.on_finish(@result)
|
|
306
313
|
|
|
@@ -463,6 +470,10 @@ module Ace
|
|
|
463
470
|
|
|
464
471
|
def handle_no_tests
|
|
465
472
|
@result = Models::TestResult.new
|
|
473
|
+
resolve_and_set_report_dir_context([])
|
|
474
|
+
reserve_execution_report
|
|
475
|
+
save_reports(@report_generator.generate(@result, [])) if @configuration.save_reports
|
|
476
|
+
@execution_evidence.publish(@result, files: [], report_dir: @execution_report_dir)
|
|
466
477
|
|
|
467
478
|
message = if @configuration.filter
|
|
468
479
|
"No test files found matching pattern '#{@configuration.filter}'"
|
|
@@ -471,6 +482,7 @@ module Ace
|
|
|
471
482
|
end
|
|
472
483
|
|
|
473
484
|
puts message
|
|
485
|
+
puts(@execution_report_dir ? "Details: #{@execution_report_dir}/" : "No saved report")
|
|
474
486
|
0
|
|
475
487
|
end
|
|
476
488
|
|
|
@@ -507,6 +519,13 @@ module Ace
|
|
|
507
519
|
end
|
|
508
520
|
end
|
|
509
521
|
|
|
522
|
+
def validate_selected_execution
|
|
523
|
+
return unless @result.success? && @result.total_tests == 0
|
|
524
|
+
|
|
525
|
+
@result.execution_success = false
|
|
526
|
+
@result.stderr = [@result.stderr, "Selected test files produced no executed tests"].reject(&:empty?).join("\n")
|
|
527
|
+
end
|
|
528
|
+
|
|
510
529
|
def build_result(parsed_result, execution_result, start_time)
|
|
511
530
|
Models::TestResult.new(
|
|
512
531
|
passed: parsed_result[:summary][:passed],
|
|
@@ -519,17 +538,16 @@ module Ace
|
|
|
519
538
|
end_time: Time.now,
|
|
520
539
|
deprecations: parsed_result[:deprecations],
|
|
521
540
|
raw_output: execution_result[:stdout],
|
|
522
|
-
stderr: execution_result[:stderr]
|
|
541
|
+
stderr: execution_result[:stderr],
|
|
542
|
+
execution_success: execution_result[:success] == true
|
|
523
543
|
)
|
|
524
544
|
end
|
|
525
545
|
|
|
526
|
-
def aggregate_individual_results(
|
|
527
|
-
#
|
|
528
|
-
individual_outputs = combined_output.split(/^Started with run options/)
|
|
529
|
-
individual_outputs.shift if individual_outputs.first && individual_outputs.first.empty?
|
|
546
|
+
def aggregate_individual_results(individual_outputs)
|
|
547
|
+
# Process boundaries come from the executor, never text inside stdout.
|
|
530
548
|
|
|
531
549
|
aggregated = {
|
|
532
|
-
raw_output:
|
|
550
|
+
raw_output: individual_outputs.join("\n"),
|
|
533
551
|
summary: {
|
|
534
552
|
runs: 0,
|
|
535
553
|
assertions: 0,
|
|
@@ -545,7 +563,6 @@ module Ace
|
|
|
545
563
|
}
|
|
546
564
|
|
|
547
565
|
individual_outputs.each do |output|
|
|
548
|
-
output = "Started with run options" + output # Restore the split text
|
|
549
566
|
parsed = @result_parser.parse_output(output)
|
|
550
567
|
|
|
551
568
|
# Sum up the counts
|
|
@@ -602,19 +619,22 @@ module Ace
|
|
|
602
619
|
)
|
|
603
620
|
end
|
|
604
621
|
|
|
622
|
+
def reserve_execution_report
|
|
623
|
+
return unless @configuration.save_reports
|
|
624
|
+
|
|
625
|
+
@report_storage = Molecules::ReportStorage.new(base_dir: @configuration.report_dir)
|
|
626
|
+
@execution_report_dir = @report_storage.reserve(@execution_evidence.identity)
|
|
627
|
+
end
|
|
628
|
+
|
|
605
629
|
def save_reports(report)
|
|
606
|
-
|
|
607
|
-
storage = Molecules::ReportStorage.new(
|
|
608
|
-
base_dir: @configuration.report_dir,
|
|
609
|
-
timestamp_generator: timestamp_generator
|
|
610
|
-
)
|
|
630
|
+
storage = @report_storage
|
|
611
631
|
|
|
612
632
|
# Save in appropriate format
|
|
613
633
|
report_path = case @configuration.format
|
|
614
634
|
when "json"
|
|
615
|
-
storage.save_report(report, format: :json)
|
|
635
|
+
storage.save_report(report, format: :json, report_dir: @execution_report_dir)
|
|
616
636
|
else
|
|
617
|
-
storage.save_report(report, format: :all)
|
|
637
|
+
storage.save_report(report, format: :all, report_dir: @execution_report_dir)
|
|
618
638
|
end
|
|
619
639
|
|
|
620
640
|
# 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["
|
|
37
|
-
@package_status[package["
|
|
38
|
-
print_package_line(package["
|
|
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["
|
|
49
|
-
print_package_line(package["
|
|
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["
|
|
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,
|
|
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["
|
|
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
|
-
#
|
|
118
|
-
|
|
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
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
success
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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
|
|
@@ -1,42 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
3
|
module Ace
|
|
6
4
|
module TestRunner
|
|
7
5
|
module Suite
|
|
8
6
|
class ResultAggregator
|
|
9
7
|
attr_reader :packages
|
|
10
8
|
|
|
11
|
-
def initialize(packages,
|
|
9
|
+
def initialize(packages, runtime_results: {})
|
|
12
10
|
@packages = packages
|
|
13
|
-
@report_root = report_root
|
|
14
11
|
@runtime_results = runtime_results || {}
|
|
15
12
|
end
|
|
16
13
|
|
|
17
14
|
def aggregate
|
|
18
15
|
results = collect_results
|
|
19
16
|
|
|
20
|
-
# Calculate assertion totals
|
|
21
|
-
total_assertions = 0
|
|
22
|
-
assertions_failed = 0
|
|
23
|
-
|
|
24
|
-
results.each do |r|
|
|
25
|
-
# Get assertions from either summary or report data
|
|
26
|
-
if r[:assertions]
|
|
27
|
-
total_assertions += r[:assertions]
|
|
28
|
-
elsif r[:report_data]
|
|
29
|
-
total_assertions += r[:report_data].dig(:result, :assertions) || 0
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
17
|
{
|
|
34
18
|
total_tests: results.sum { |r| r[:total] || 0 },
|
|
35
19
|
total_passed: results.sum { |r| r[:passed] || 0 },
|
|
36
20
|
total_failed: results.sum { |r| (r[:failed] || 0) + (r[:errors] || 0) },
|
|
37
21
|
total_skipped: results.sum { |r| r[:skipped] || 0 },
|
|
38
|
-
total_assertions:
|
|
39
|
-
assertions_failed:
|
|
22
|
+
total_assertions: results.sum { |result| result[:assertions] || 0 },
|
|
23
|
+
assertions_failed: 0,
|
|
40
24
|
total_duration: results.map { |r| r[:duration] || 0 }.max,
|
|
41
25
|
packages_passed: results.count { |r| r[:success] },
|
|
42
26
|
packages_failed: results.count { |r| !r[:success] },
|
|
@@ -47,81 +31,22 @@ module Ace
|
|
|
47
31
|
|
|
48
32
|
def collect_results
|
|
49
33
|
@packages.map do |package|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
report_root: @report_root,
|
|
56
|
-
package_name: package["name"]
|
|
57
|
-
)
|
|
58
|
-
summary_path = reports_dir ? File.join(reports_dir, "summary.json") : nil
|
|
59
|
-
|
|
60
|
-
if summary_path && File.exist?(summary_path)
|
|
61
|
-
begin
|
|
62
|
-
data = JSON.parse(File.read(summary_path), symbolize_names: true)
|
|
63
|
-
data[:package] = package["name"]
|
|
64
|
-
data[:path] = package["path"]
|
|
65
|
-
data[:report_root] = @report_root
|
|
66
|
-
|
|
67
|
-
# Try to get assertions from report.json if not in summary
|
|
68
|
-
if !data[:assertions] || data[:assertions] == 0
|
|
69
|
-
report_path = File.join(reports_dir, "report.json")
|
|
70
|
-
if File.exist?(report_path)
|
|
71
|
-
begin
|
|
72
|
-
report_data = JSON.parse(File.read(report_path), symbolize_names: true)
|
|
73
|
-
data[:assertions] = report_data.dig(:result, :assertions) || 0
|
|
74
|
-
data[:report_data] = report_data
|
|
75
|
-
rescue JSON::ParserError
|
|
76
|
-
# Ignore if report.json can't be parsed
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
data
|
|
82
|
-
rescue JSON::ParserError => e
|
|
83
|
-
# If we can't parse the summary, create a failure result
|
|
84
|
-
{
|
|
85
|
-
package: package["name"],
|
|
86
|
-
path: package["path"],
|
|
87
|
-
report_root: @report_root,
|
|
88
|
-
success: false,
|
|
89
|
-
error: "Failed to parse summary.json: #{e.message}",
|
|
90
|
-
total: 0,
|
|
91
|
-
passed: 0,
|
|
92
|
-
failed: 0,
|
|
93
|
-
errors: 1
|
|
94
|
-
}
|
|
95
|
-
end
|
|
96
|
-
else
|
|
97
|
-
runtime || {
|
|
98
|
-
package: package["name"],
|
|
99
|
-
path: package["path"],
|
|
100
|
-
report_root: @report_root,
|
|
101
|
-
success: false,
|
|
102
|
-
error: "No test results found (summary.json missing)",
|
|
103
|
-
total: 0,
|
|
104
|
-
passed: 0,
|
|
105
|
-
failed: 0,
|
|
106
|
-
errors: 1
|
|
107
|
-
}
|
|
108
|
-
end
|
|
34
|
+
runtime_result(package) || {
|
|
35
|
+
package: package["name"], path: package["path"], entry_id: package["entry_id"],
|
|
36
|
+
report_dir: nil, success: false, error: "No verified execution completion evidence",
|
|
37
|
+
total: 0, passed: 0, failed: 0, errors: 1, assertions: 0
|
|
38
|
+
}
|
|
109
39
|
end
|
|
110
40
|
end
|
|
111
41
|
|
|
112
|
-
def runtime_result_overrides_summary?(package, runtime)
|
|
113
|
-
return false unless runtime
|
|
114
|
-
|
|
115
|
-
status = @runtime_results[package["name"]] || {}
|
|
116
|
-
status[:timed_out] || status[:interrupted] || runtime[:success] == false
|
|
117
|
-
end
|
|
118
|
-
|
|
119
42
|
def collect_failed_packages(results)
|
|
120
43
|
results.select { |r| !r[:success] }.map do |result|
|
|
121
44
|
{
|
|
122
45
|
name: result[:package],
|
|
123
46
|
path: result[:path],
|
|
124
|
-
|
|
47
|
+
report_dir: result[:report_dir],
|
|
48
|
+
execution_id: result[:execution_id],
|
|
49
|
+
entry_id: result[:entry_id],
|
|
125
50
|
failures: result[:failed] || 0,
|
|
126
51
|
errors: result[:errors] || 0,
|
|
127
52
|
error_message: result[:error]
|
|
@@ -130,7 +55,7 @@ module Ace
|
|
|
130
55
|
end
|
|
131
56
|
|
|
132
57
|
def runtime_result(package)
|
|
133
|
-
status = @runtime_results[package["
|
|
58
|
+
status = @runtime_results[package["entry_id"]]
|
|
134
59
|
return nil unless status && status[:completed]
|
|
135
60
|
|
|
136
61
|
results = status[:results] || {}
|
|
@@ -142,11 +67,14 @@ module Ace
|
|
|
142
67
|
{
|
|
143
68
|
package: package["name"],
|
|
144
69
|
path: package["path"],
|
|
145
|
-
|
|
70
|
+
report_dir: results[:report_dir],
|
|
71
|
+
completion_path: results[:completion_path],
|
|
72
|
+
execution_id: results[:execution_id],
|
|
73
|
+
entry_id: package["entry_id"],
|
|
146
74
|
success: status[:success],
|
|
147
75
|
error: results[:error],
|
|
148
76
|
total: total,
|
|
149
|
-
passed:
|
|
77
|
+
passed: results[:passed] || 0,
|
|
150
78
|
failed: failures,
|
|
151
79
|
errors: errors,
|
|
152
80
|
skipped: skipped,
|
|
@@ -31,7 +31,7 @@ module Ace
|
|
|
31
31
|
|
|
32
32
|
# Called when a package status changes. Prints a line when package completes.
|
|
33
33
|
def update_package(package, status, _output = nil)
|
|
34
|
-
@package_status[package["
|
|
34
|
+
@package_status[package["entry_id"]] = status
|
|
35
35
|
|
|
36
36
|
# Only print when package completes
|
|
37
37
|
return unless status[:completed]
|
|
@@ -88,6 +88,7 @@ module Ace
|
|
|
88
88
|
line += " #{skipped} skip" if skipped > 0
|
|
89
89
|
line += " timeout" if status[:timed_out]
|
|
90
90
|
|
|
91
|
+
line += results[:report_dir] ? " report: #{results[:report_dir]}" : " no saved report"
|
|
91
92
|
puts line
|
|
92
93
|
end
|
|
93
94
|
|
data/lib/ace/test_runner.rb
CHANGED
|
@@ -45,6 +45,7 @@ require_relative "test_runner/atoms/environment_sanitizer"
|
|
|
45
45
|
require_relative "test_runner/molecules/fixture_environment"
|
|
46
46
|
require_relative "test_runner/molecules/test_executor"
|
|
47
47
|
require_relative "test_runner/molecules/failure_analyzer"
|
|
48
|
+
require_relative "test_runner/molecules/execution_evidence"
|
|
48
49
|
require_relative "test_runner/molecules/report_storage"
|
|
49
50
|
require_relative "test_runner/molecules/config_loader"
|
|
50
51
|
require_relative "test_runner/molecules/pattern_resolver"
|