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.
@@ -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
 
@@ -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(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?
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: combined_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
- timestamp_generator = Atoms::TimestampGenerator.new
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["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
@@ -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, report_root: nil, runtime_results: {})
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: total_assertions,
39
- assertions_failed: 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
- runtime = runtime_result(package)
51
- next runtime if runtime_result_overrides_summary?(package, runtime)
52
-
53
- reports_dir = Atoms::ReportPathResolver.report_directory(
54
- package["path"],
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
- report_root: result[:report_root] || @report_root,
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["name"]]
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
- report_root: @report_root,
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: total - failures - errors - skipped,
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["name"]] = status
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
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module TestRunner
5
- VERSION = "0.27.1"
5
+ VERSION = "0.28.0"
6
6
  end
7
7
  end
@@ -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"