ace-test-runner 0.18.1 → 0.19.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/exe/ace-test-suite +9 -0
- data/lib/ace/test_runner/suite/orchestrator.rb +11 -2
- data/lib/ace/test_runner/suite/process_monitor.rb +208 -92
- data/lib/ace/test_runner/suite/result_aggregator.rb +29 -3
- data/lib/ace/test_runner/suite/simple_display_manager.rb +1 -0
- data/lib/ace/test_runner/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e03df42988ad85fabb842d9f7a5c5c018fbe14d70e085b9892a234bd20edc84
|
|
4
|
+
data.tar.gz: 351b02463eee937bb30257ffcec182160b1925ac63a3347585fc335e7e12a0d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1113c8e85abf9352bf8bfeb10e1d563bb197a5fedebacd3bd934b945a9830ad312c2e830e0b93cd09d4bf038e9f4fd113b7179fbce210e0b8bc801a4d58cf29b
|
|
7
|
+
data.tar.gz: 5c14a5221c6c4f1af586eb8c7811a007d555a09697a491d3578e45ca99e53a3047c620a756aaed53ab31df6be082c7756caa590dbfed258fbd810be3d02325c9
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.19.1] - 2026-03-29
|
|
11
|
+
|
|
12
|
+
### Technical
|
|
13
|
+
- Normalized published gem metadata so RubyGems and Ruby Toolbox use current release information instead of the 1980 fallback date.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- **ace-test-runner v0.19.0**: Added suite-level per-package timeout support via `ace-test-suite --timeout` and `test_suite.timeout`, allowing hung package runs to fail and free their worker slot instead of stalling the rest of the suite.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **ace-test-runner v0.19.0**: Terminate active package process groups on suite timeout or interrupt so `ace-test-suite` no longer leaves orphaned `ace-test` subprocesses behind after stuck or cancelled runs.
|
|
10
20
|
|
|
11
21
|
## [0.18.1] - 2026-03-29
|
|
12
22
|
|
data/exe/ace-test-suite
CHANGED
|
@@ -38,6 +38,10 @@ parser = OptionParser.new do |opts|
|
|
|
38
38
|
options[:group] = group
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
opts.on("-t", "--timeout SEC", Integer, "Per-package timeout in seconds") do |timeout|
|
|
42
|
+
options[:timeout] = timeout
|
|
43
|
+
end
|
|
44
|
+
|
|
41
45
|
opts.on("-v", "--verbose", "Show verbose output") do
|
|
42
46
|
options[:verbose] = true
|
|
43
47
|
end
|
|
@@ -57,6 +61,7 @@ parser = OptionParser.new do |opts|
|
|
|
57
61
|
puts " $ ace-test-suite Run all (simple line-by-line output)"
|
|
58
62
|
puts " $ ace-test-suite --progress Show live animated progress bars"
|
|
59
63
|
puts " $ ace-test-suite --parallel 16 Run with 16 parallel processes"
|
|
64
|
+
puts " $ ace-test-suite --timeout 10 Fail packages that run over 10s"
|
|
60
65
|
puts " $ ace-test-suite --group foundation Run only foundation group"
|
|
61
66
|
puts " $ ace-test-suite --verbose Show detailed test output"
|
|
62
67
|
exit 0
|
|
@@ -110,6 +115,10 @@ if options[:parallel]
|
|
|
110
115
|
config["test_suite"]["max_parallel"] = options[:parallel]
|
|
111
116
|
end
|
|
112
117
|
|
|
118
|
+
if options[:timeout]
|
|
119
|
+
config["test_suite"]["timeout"] = options[:timeout]
|
|
120
|
+
end
|
|
121
|
+
|
|
113
122
|
if options[:group]
|
|
114
123
|
config["test_suite"]["packages"].select! { |p| p["group"] == options[:group] }
|
|
115
124
|
if config["test_suite"]["packages"].empty?
|
|
@@ -33,7 +33,10 @@ module Ace
|
|
|
33
33
|
test_options["report_dir"] = report_root if report_root
|
|
34
34
|
|
|
35
35
|
display_manager = create_display_manager
|
|
36
|
-
process_monitor = ProcessMonitor.new(
|
|
36
|
+
process_monitor = ProcessMonitor.new(
|
|
37
|
+
@config.dig("test_suite", "max_parallel") || 10,
|
|
38
|
+
package_timeout: @config.dig("test_suite", "timeout")
|
|
39
|
+
)
|
|
37
40
|
|
|
38
41
|
# Enrich packages with historical duration data for scheduling
|
|
39
42
|
estimator = DurationEstimator.new(report_root: report_root)
|
|
@@ -69,13 +72,19 @@ module Ace
|
|
|
69
72
|
display_manager.show_final_results
|
|
70
73
|
|
|
71
74
|
# Aggregate results
|
|
72
|
-
aggregator = ResultAggregator.new(@packages, report_root: report_root)
|
|
75
|
+
aggregator = ResultAggregator.new(@packages, report_root: report_root, runtime_results: @results)
|
|
73
76
|
summary = aggregator.aggregate
|
|
74
77
|
|
|
75
78
|
display_manager.show_summary(summary)
|
|
76
79
|
|
|
77
80
|
# Return exit code based on results
|
|
78
81
|
(summary[:packages_failed] > 0) ? 1 : 0
|
|
82
|
+
rescue Interrupt
|
|
83
|
+
process_monitor&.stop_all(reason: :interrupt)
|
|
84
|
+
raise
|
|
85
|
+
rescue StandardError
|
|
86
|
+
process_monitor&.stop_all(reason: :error)
|
|
87
|
+
raise
|
|
79
88
|
end
|
|
80
89
|
|
|
81
90
|
private
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
4
|
require "json"
|
|
5
|
+
require "timeout"
|
|
5
6
|
|
|
6
7
|
module Ace
|
|
7
8
|
module TestRunner
|
|
@@ -9,8 +10,11 @@ module Ace
|
|
|
9
10
|
class ProcessMonitor
|
|
10
11
|
attr_reader :processes, :max_parallel
|
|
11
12
|
|
|
12
|
-
def initialize(max_parallel = 10)
|
|
13
|
+
def initialize(max_parallel = 10, package_timeout: nil, termination_grace_period: 1.0, clock: nil)
|
|
13
14
|
@max_parallel = max_parallel
|
|
15
|
+
@package_timeout = package_timeout
|
|
16
|
+
@termination_grace_period = termination_grace_period
|
|
17
|
+
@clock = clock || -> { Time.now }
|
|
14
18
|
@processes = {}
|
|
15
19
|
@queue = []
|
|
16
20
|
@completed = []
|
|
@@ -33,8 +37,8 @@ module Ace
|
|
|
33
37
|
"ACE_ASSIGN_ID" => nil,
|
|
34
38
|
"ACE_ASSIGN_FORK_ROOT" => nil
|
|
35
39
|
})
|
|
36
|
-
start_time =
|
|
37
|
-
stdin, stdout, stderr, thread = Open3.popen3(env, cmd, chdir: package["path"])
|
|
40
|
+
start_time = now
|
|
41
|
+
stdin, stdout, stderr, thread = Open3.popen3(env, *cmd, chdir: package["path"], pgroup: true)
|
|
38
42
|
|
|
39
43
|
@processes[package["name"]] = {
|
|
40
44
|
package: package,
|
|
@@ -45,10 +49,18 @@ module Ace
|
|
|
45
49
|
start_time: start_time,
|
|
46
50
|
callback: callback,
|
|
47
51
|
output: +"",
|
|
52
|
+
stderr_output: +"",
|
|
48
53
|
report_root: test_options["report_dir"],
|
|
49
54
|
test_count: 0,
|
|
50
55
|
tests_run: 0,
|
|
51
|
-
dots: +""
|
|
56
|
+
dots: +"",
|
|
57
|
+
timeout: @package_timeout,
|
|
58
|
+
pid: thread.pid,
|
|
59
|
+
pgid: thread.pid,
|
|
60
|
+
terminating: false,
|
|
61
|
+
timeout_triggered: false,
|
|
62
|
+
terminated_by: nil,
|
|
63
|
+
terminate_at: nil
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
# Initial callback
|
|
@@ -61,96 +73,32 @@ module Ace
|
|
|
61
73
|
thread = process_info[:thread]
|
|
62
74
|
callback = process_info[:callback]
|
|
63
75
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
progress: process_info[:tests_run],
|
|
79
|
-
total: process_info[:test_count],
|
|
80
|
-
dots: process_info[:dots],
|
|
81
|
-
elapsed: elapsed
|
|
82
|
-
}, chunk)
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
rescue IO::WaitReadable, EOFError
|
|
86
|
-
# No data available or stream closed
|
|
76
|
+
stdout_chunk = drain_stream(process_info[:stdout], process_info[:output])
|
|
77
|
+
stderr_chunk = drain_stream(process_info[:stderr], process_info[:stderr_output])
|
|
78
|
+
|
|
79
|
+
parse_progress(process_info, stdout_chunk) if stdout_chunk && !stdout_chunk.empty?
|
|
80
|
+
|
|
81
|
+
if callback && ((stdout_chunk && !stdout_chunk.empty?) || (stderr_chunk && !stderr_chunk.empty?))
|
|
82
|
+
elapsed = now - process_info[:start_time]
|
|
83
|
+
callback.call(package, {
|
|
84
|
+
status: :running,
|
|
85
|
+
progress: process_info[:tests_run],
|
|
86
|
+
total: process_info[:test_count],
|
|
87
|
+
dots: process_info[:dots],
|
|
88
|
+
elapsed: elapsed
|
|
89
|
+
}, stdout_chunk)
|
|
87
90
|
end
|
|
88
91
|
|
|
92
|
+
enforce_timeout(process_info)
|
|
93
|
+
|
|
89
94
|
# Check if process completed
|
|
90
95
|
unless thread.alive?
|
|
91
|
-
elapsed =
|
|
96
|
+
elapsed = now - process_info[:start_time]
|
|
92
97
|
exit_status = thread.value.exitstatus
|
|
93
98
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
rescue
|
|
98
|
-
""
|
|
99
|
-
end
|
|
100
|
-
process_info[:output] << remaining_output
|
|
101
|
-
|
|
102
|
-
# Try to get accurate results from summary.json first
|
|
103
|
-
results = nil
|
|
104
|
-
reports_dir = Atoms::ReportPathResolver.report_directory(
|
|
105
|
-
package["path"],
|
|
106
|
-
report_root: process_info[:report_root],
|
|
107
|
-
package_name: package["name"]
|
|
108
|
-
)
|
|
109
|
-
summary_file = reports_dir ? File.join(reports_dir, "summary.json") : nil
|
|
110
|
-
if summary_file && File.exist?(summary_file)
|
|
111
|
-
begin
|
|
112
|
-
json_data = JSON.parse(File.read(summary_file))
|
|
113
|
-
results = {
|
|
114
|
-
tests: json_data["total"] || 0,
|
|
115
|
-
assertions: json_data["assertions"] || 0,
|
|
116
|
-
failures: json_data["failed"] || 0,
|
|
117
|
-
errors: json_data["errors"] || 0,
|
|
118
|
-
duration: json_data["duration"] || elapsed,
|
|
119
|
-
success: json_data["success"] || false
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
# Also try to get assertions from report.json if not in summary
|
|
123
|
-
if results[:assertions] == 0
|
|
124
|
-
report_file = File.join(reports_dir, "report.json")
|
|
125
|
-
if File.exist?(report_file)
|
|
126
|
-
report_data = JSON.parse(File.read(report_file))
|
|
127
|
-
results[:assertions] = report_data.dig("result", "assertions") || 0
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
rescue JSON::ParserError
|
|
131
|
-
# Fall back to parsing output
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Fall back to parsing output if no JSON data
|
|
136
|
-
results ||= parse_results(process_info[:output])
|
|
137
|
-
|
|
138
|
-
# Close streams
|
|
139
|
-
begin
|
|
140
|
-
process_info[:stdout].close
|
|
141
|
-
rescue
|
|
142
|
-
nil
|
|
143
|
-
end
|
|
144
|
-
begin
|
|
145
|
-
process_info[:stderr].close
|
|
146
|
-
rescue
|
|
147
|
-
nil
|
|
148
|
-
end
|
|
149
|
-
begin
|
|
150
|
-
process_info[:stdin].close
|
|
151
|
-
rescue
|
|
152
|
-
nil
|
|
153
|
-
end
|
|
99
|
+
collect_remaining_output(process_info)
|
|
100
|
+
results = build_results(process_info, elapsed)
|
|
101
|
+
close_streams(process_info)
|
|
154
102
|
|
|
155
103
|
# Final callback
|
|
156
104
|
if callback
|
|
@@ -164,6 +112,8 @@ module Ace
|
|
|
164
112
|
success: success_status,
|
|
165
113
|
exit_code: exit_status,
|
|
166
114
|
elapsed: elapsed,
|
|
115
|
+
timed_out: process_info[:timeout_triggered],
|
|
116
|
+
interrupted: process_info[:terminated_by] == :interrupt,
|
|
167
117
|
results: results
|
|
168
118
|
}, process_info[:output])
|
|
169
119
|
end
|
|
@@ -188,6 +138,38 @@ module Ace
|
|
|
188
138
|
!@processes.empty? || !@queue.empty?
|
|
189
139
|
end
|
|
190
140
|
|
|
141
|
+
def stop_all(reason: :interrupt)
|
|
142
|
+
@queue.clear
|
|
143
|
+
|
|
144
|
+
@processes.each_value do |process_info|
|
|
145
|
+
terminate_process_group(process_info, signal: "TERM", reason: reason)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
deadline = now + @termination_grace_period
|
|
149
|
+
while @processes.values.any? { |info| info[:thread].alive? } && now < deadline
|
|
150
|
+
sleep 0.05
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
@processes.each_value do |process_info|
|
|
154
|
+
next unless process_info[:thread].alive?
|
|
155
|
+
|
|
156
|
+
terminate_process_group(process_info, signal: "KILL", reason: reason)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
@processes.each_value do |process_info|
|
|
160
|
+
begin
|
|
161
|
+
Timeout.timeout(0.5) { process_info[:thread].value if process_info[:thread].alive? }
|
|
162
|
+
rescue Timeout::Error, StandardError
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
close_streams(process_info)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
@processes.clear
|
|
170
|
+
@completed.clear
|
|
171
|
+
end
|
|
172
|
+
|
|
191
173
|
def wait_all
|
|
192
174
|
while running?
|
|
193
175
|
check_processes
|
|
@@ -219,10 +201,144 @@ module Ace
|
|
|
219
201
|
cmd_parts << "--report-dir" << pkg_report_dir
|
|
220
202
|
end
|
|
221
203
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
204
|
+
cmd_parts
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def now
|
|
208
|
+
@clock.call
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def enforce_timeout(process_info)
|
|
212
|
+
timeout = process_info[:timeout]
|
|
213
|
+
return unless timeout
|
|
214
|
+
|
|
215
|
+
elapsed = now - process_info[:start_time]
|
|
216
|
+
|
|
217
|
+
if !process_info[:timeout_triggered] && elapsed > timeout
|
|
218
|
+
process_info[:timeout_triggered] = true
|
|
219
|
+
process_info[:terminate_at] = now + @termination_grace_period
|
|
220
|
+
terminate_process_group(process_info, signal: "TERM", reason: :timeout)
|
|
221
|
+
elsif process_info[:timeout_triggered] && process_info[:thread].alive? && process_info[:terminate_at] && now >= process_info[:terminate_at]
|
|
222
|
+
terminate_process_group(process_info, signal: "KILL", reason: :timeout)
|
|
223
|
+
process_info[:terminate_at] = nil
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def terminate_process_group(process_info, signal:, reason:)
|
|
228
|
+
process_info[:terminated_by] = reason
|
|
229
|
+
process_info[:terminating] = true
|
|
230
|
+
Process.kill(signal, -process_info[:pgid])
|
|
231
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def drain_stream(io, buffer)
|
|
236
|
+
return nil unless io && !io.closed?
|
|
237
|
+
|
|
238
|
+
chunk = +""
|
|
239
|
+
loop do
|
|
240
|
+
ready = IO.select([io], nil, nil, 0)
|
|
241
|
+
break unless ready
|
|
242
|
+
|
|
243
|
+
chunk << io.read_nonblock(4096)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
buffer << chunk unless chunk.empty?
|
|
247
|
+
chunk
|
|
248
|
+
rescue IO::WaitReadable, EOFError
|
|
249
|
+
buffer << chunk unless chunk.empty?
|
|
250
|
+
chunk
|
|
251
|
+
rescue IOError
|
|
252
|
+
nil
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def collect_remaining_output(process_info)
|
|
256
|
+
process_info[:output] << safe_read(process_info[:stdout])
|
|
257
|
+
process_info[:stderr_output] << safe_read(process_info[:stderr])
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def safe_read(io)
|
|
261
|
+
return "" unless io && !io.closed?
|
|
262
|
+
|
|
263
|
+
io.read
|
|
264
|
+
rescue StandardError
|
|
265
|
+
""
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def close_streams(process_info)
|
|
269
|
+
%i[stdout stderr stdin].each do |stream|
|
|
270
|
+
begin
|
|
271
|
+
process_info[stream]&.close
|
|
272
|
+
rescue StandardError
|
|
273
|
+
nil
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def build_results(process_info, elapsed)
|
|
279
|
+
return timeout_results(process_info, elapsed) if process_info[:timeout_triggered]
|
|
280
|
+
return interrupted_results(elapsed) if process_info[:terminated_by] == :interrupt
|
|
281
|
+
results = load_summary_results(process_info, elapsed)
|
|
282
|
+
return results if results
|
|
283
|
+
|
|
284
|
+
parse_results(process_info[:output])
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def timeout_results(process_info, elapsed)
|
|
288
|
+
{
|
|
289
|
+
tests: 0,
|
|
290
|
+
assertions: 0,
|
|
291
|
+
failures: 0,
|
|
292
|
+
errors: 1,
|
|
293
|
+
duration: elapsed,
|
|
294
|
+
success: false,
|
|
295
|
+
error: "Timed out after #{process_info[:timeout]} seconds"
|
|
296
|
+
}
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def interrupted_results(elapsed)
|
|
300
|
+
{
|
|
301
|
+
tests: 0,
|
|
302
|
+
assertions: 0,
|
|
303
|
+
failures: 0,
|
|
304
|
+
errors: 1,
|
|
305
|
+
duration: elapsed,
|
|
306
|
+
success: false,
|
|
307
|
+
error: "Interrupted before completion"
|
|
308
|
+
}
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def load_summary_results(process_info, elapsed)
|
|
312
|
+
package = process_info[:package]
|
|
313
|
+
reports_dir = Atoms::ReportPathResolver.report_directory(
|
|
314
|
+
package["path"],
|
|
315
|
+
report_root: process_info[:report_root],
|
|
316
|
+
package_name: package["name"]
|
|
317
|
+
)
|
|
318
|
+
summary_file = reports_dir ? File.join(reports_dir, "summary.json") : nil
|
|
319
|
+
return nil unless summary_file && File.exist?(summary_file)
|
|
320
|
+
|
|
321
|
+
json_data = JSON.parse(File.read(summary_file))
|
|
322
|
+
results = {
|
|
323
|
+
tests: json_data["total"] || 0,
|
|
324
|
+
assertions: json_data["assertions"] || 0,
|
|
325
|
+
failures: json_data["failed"] || 0,
|
|
326
|
+
errors: json_data["errors"] || 0,
|
|
327
|
+
duration: json_data["duration"] || elapsed,
|
|
328
|
+
success: json_data["success"] || false
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if results[:assertions] == 0
|
|
332
|
+
report_file = File.join(reports_dir, "report.json")
|
|
333
|
+
if File.exist?(report_file)
|
|
334
|
+
report_data = JSON.parse(File.read(report_file))
|
|
335
|
+
results[:assertions] = report_data.dig("result", "assertions") || 0
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
results
|
|
340
|
+
rescue JSON::ParserError
|
|
341
|
+
nil
|
|
226
342
|
end
|
|
227
343
|
|
|
228
344
|
def parse_progress(process_info, chunk)
|
|
@@ -8,9 +8,10 @@ module Ace
|
|
|
8
8
|
class ResultAggregator
|
|
9
9
|
attr_reader :packages
|
|
10
10
|
|
|
11
|
-
def initialize(packages, report_root: nil)
|
|
11
|
+
def initialize(packages, report_root: nil, runtime_results: {})
|
|
12
12
|
@packages = packages
|
|
13
13
|
@report_root = report_root
|
|
14
|
+
@runtime_results = runtime_results || {}
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def aggregate
|
|
@@ -90,8 +91,7 @@ module Ace
|
|
|
90
91
|
}
|
|
91
92
|
end
|
|
92
93
|
else
|
|
93
|
-
|
|
94
|
-
{
|
|
94
|
+
runtime_result(package) || {
|
|
95
95
|
package: package["name"],
|
|
96
96
|
path: package["path"],
|
|
97
97
|
report_root: @report_root,
|
|
@@ -119,6 +119,32 @@ module Ace
|
|
|
119
119
|
end
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
+
def runtime_result(package)
|
|
123
|
+
status = @runtime_results[package["name"]]
|
|
124
|
+
return nil unless status && status[:completed]
|
|
125
|
+
|
|
126
|
+
results = status[:results] || {}
|
|
127
|
+
total = results[:tests] || 0
|
|
128
|
+
failures = results[:failures] || 0
|
|
129
|
+
errors = results[:errors] || 0
|
|
130
|
+
skipped = results[:skipped] || 0
|
|
131
|
+
|
|
132
|
+
{
|
|
133
|
+
package: package["name"],
|
|
134
|
+
path: package["path"],
|
|
135
|
+
report_root: @report_root,
|
|
136
|
+
success: status[:success],
|
|
137
|
+
error: results[:error],
|
|
138
|
+
total: total,
|
|
139
|
+
passed: total - failures - errors - skipped,
|
|
140
|
+
failed: failures,
|
|
141
|
+
errors: errors,
|
|
142
|
+
skipped: skipped,
|
|
143
|
+
duration: results[:duration] || status[:elapsed] || 0,
|
|
144
|
+
assertions: results[:assertions] || 0
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
|
|
122
148
|
def generate_report(summary)
|
|
123
149
|
report = []
|
|
124
150
|
report << "# ACE Test Suite Report"
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-test-runner
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.19.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-03-29 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-cli
|