probatio_diabolica 0.4.4 → 0.4.5
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/README.md +12 -0
- data/bin/prd +31 -7
- data/bin/prd_mcp +2 -0
- data/lib/pr_d/formatters/html_formatter.rb +31 -4
- data/lib/pr_d/formatters/pdf_formatter.rb +3 -1
- data/lib/pr_d/helpers/chrome_helper.rb +13 -0
- data/lib/pr_d/mcp/run_specs_tool.rb +16 -1
- data/lib/pr_d/mcp/server.rb +13 -1
- data/lib/pr_d/version.rb +1 -1
- data/lib/pr_d/worker_runner.rb +103 -0
- data/lib/pr_d.rb +74 -31
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0a935022c47a87f28cab73012b6c96563616958a6eb9ff28d982ed7dc6b7abdd
|
|
4
|
+
data.tar.gz: 7b1ad8ea3bc58ce2a167508d95a8e63f574078e4f7b06e007f8650185cfc43fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a918c5542c8fb74413fd7e8fc2536767469f86833a0ec78ebe3a444156a02431d64e7c573fe10804e2ed008d53abf5ca187754fdae1fbb3ad3d416b6eec2c3c
|
|
7
|
+
data.tar.gz: 31e087b60fd08046dd952dda13e3ab30599b8f04ebc311a622bf26ada5950f00f7f81563c7ddd813c6725c5e992c8b2b7091366fea9338553b219baa8fa44f1c
|
data/README.md
CHANGED
|
@@ -79,6 +79,7 @@ Input:
|
|
|
79
79
|
- `out` (optional): same as `-o`
|
|
80
80
|
- `formatters` (optional): array of `simple|html|json|pdf` (default: `["simple"]`)
|
|
81
81
|
- `mode` (optional): `verbose|synthetic` (default: `synthetic`)
|
|
82
|
+
- `jobs` (optional): file-level workers count (integer >= 1, default: `1`)
|
|
82
83
|
|
|
83
84
|
Output (`structuredContent`):
|
|
84
85
|
- `ok`, `exit_code`
|
|
@@ -95,6 +96,7 @@ Options:
|
|
|
95
96
|
- `-o, --out PATH` output base path (directory or file-like base name)
|
|
96
97
|
- `-m, --mode MODE` output mode, default: `verbose`
|
|
97
98
|
- supported: `verbose`, `synthetic`
|
|
99
|
+
- `-j, --jobs N` number of workers for file-level execution, default: `1`
|
|
98
100
|
|
|
99
101
|
Output rules (`--out`):
|
|
100
102
|
|
|
@@ -131,8 +133,18 @@ prd examples/basics_spec.rb -t html,json,pdf -o ./tmp/my_report
|
|
|
131
133
|
|
|
132
134
|
# compact synthetic output on console
|
|
133
135
|
prd examples/basics_spec.rb --mode synthetic
|
|
136
|
+
|
|
137
|
+
# run a directory with 4 file-level workers
|
|
138
|
+
prd spec --mode synthetic --jobs 4
|
|
134
139
|
```
|
|
135
140
|
|
|
141
|
+
Parallel execution notes:
|
|
142
|
+
|
|
143
|
+
- Parallelism is file-level only (`*_spec.rb` files are distributed across workers).
|
|
144
|
+
- Event rendering order stays deterministic: files are merged in sorted path order.
|
|
145
|
+
- `--jobs 1` uses the same worker pipeline as `--jobs N`.
|
|
146
|
+
- Specs sharing mutable global state across files can become non-deterministic when `jobs > 1`.
|
|
147
|
+
|
|
136
148
|
## Available DSL
|
|
137
149
|
|
|
138
150
|
It is inspired by RSpec but with a custom runtime and additional features.
|
data/bin/prd
CHANGED
|
@@ -19,6 +19,7 @@ FORMATTERS = {
|
|
|
19
19
|
SUPPORTED_FORMATTERS = FORMATTERS.keys.freeze
|
|
20
20
|
SUPPORTED_MODES = %w[verbose synthetic].freeze
|
|
21
21
|
DEFAULT_REPORT_BASENAME = 'report'.freeze
|
|
22
|
+
DEFAULT_JOBS = 1
|
|
22
23
|
|
|
23
24
|
class CompositeFormatter
|
|
24
25
|
def initialize(formatters)
|
|
@@ -68,6 +69,17 @@ def normalize_mode(raw_mode)
|
|
|
68
69
|
mode
|
|
69
70
|
end
|
|
70
71
|
|
|
72
|
+
def normalize_jobs(raw_jobs)
|
|
73
|
+
return DEFAULT_JOBS if raw_jobs.nil?
|
|
74
|
+
|
|
75
|
+
jobs = Integer(raw_jobs)
|
|
76
|
+
raise "Invalid jobs count: #{jobs}. Expected an integer greater than or equal to 1." if jobs < 1
|
|
77
|
+
|
|
78
|
+
jobs
|
|
79
|
+
rescue ArgumentError, TypeError
|
|
80
|
+
raise "Invalid jobs value: #{raw_jobs}. Expected an integer greater than or equal to 1."
|
|
81
|
+
end
|
|
82
|
+
|
|
71
83
|
def directory_like_path?(path)
|
|
72
84
|
path.end_with?(File::SEPARATOR) || Dir.exist?(path)
|
|
73
85
|
end
|
|
@@ -98,6 +110,9 @@ OptionParser
|
|
|
98
110
|
opts.on('-m', '--mode MODE', 'Specify output mode (verbose, synthetic)') do |mode|
|
|
99
111
|
options[:mode] = mode
|
|
100
112
|
end
|
|
113
|
+
opts.on('-j', '--jobs N', 'Specify number of workers (default: 1)') do |jobs|
|
|
114
|
+
options[:jobs] = jobs
|
|
115
|
+
end
|
|
101
116
|
end
|
|
102
117
|
.parse!
|
|
103
118
|
|
|
@@ -106,17 +121,18 @@ raise 'No tests found. Please specify a test file or directory (e.g. prd example
|
|
|
106
121
|
raise "Unexpected arguments: #{ARGV.join(' ')}" unless ARGV.empty?
|
|
107
122
|
raise "Path not found: #{input_path}" unless File.exist?(input_path)
|
|
108
123
|
|
|
109
|
-
|
|
124
|
+
test_files =
|
|
110
125
|
if File.directory?(input_path)
|
|
111
126
|
files = Dir[File.join(input_path, '**', '*_spec.rb')].sort
|
|
112
127
|
raise "No spec files found in directory: #{input_path}" if files.empty?
|
|
113
|
-
files
|
|
128
|
+
files
|
|
114
129
|
elsif File.file?(input_path)
|
|
115
|
-
[
|
|
130
|
+
[input_path]
|
|
116
131
|
end
|
|
117
132
|
|
|
118
133
|
formatter_types = parse_formatter_types(options[:formatters])
|
|
119
134
|
mode = normalize_mode(options[:mode])
|
|
135
|
+
jobs = normalize_jobs(options[:jobs])
|
|
120
136
|
out_base = output_base_path(options[:out])
|
|
121
137
|
|
|
122
138
|
if formatter_types.include?('pdf') && out_base.nil?
|
|
@@ -169,15 +185,23 @@ collector = PrD::ReportCollector.new(
|
|
|
169
185
|
)
|
|
170
186
|
|
|
171
187
|
runtime_output_dir = out_base.nil? ? nil : File.dirname(out_base)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
188
|
+
runner_result = PrD::WorkerRunner.new(
|
|
189
|
+
file_paths: test_files,
|
|
190
|
+
jobs: jobs,
|
|
191
|
+
mode: mode.to_sym,
|
|
192
|
+
serializers: serializers,
|
|
193
|
+
output_dir: runtime_output_dir,
|
|
194
|
+
config_file: options[:config],
|
|
195
|
+
subject_display_strategy: collector.subject_display_strategy,
|
|
196
|
+
eager_subject_display_strategy: collector.eager_subject_display_strategy
|
|
197
|
+
).run
|
|
198
|
+
exit_status = runner_result.failed_count.zero? ? 0 : 1
|
|
175
199
|
|
|
176
200
|
destinations.each do |destination|
|
|
177
201
|
io = destination[:io]
|
|
178
202
|
type = destination[:formatter_type]
|
|
179
203
|
formatter = FORMATTERS.fetch(type)[:klass].new(io:, serializers:, mode: mode.to_sym)
|
|
180
|
-
PrD::ReportRenderer.render(
|
|
204
|
+
PrD::ReportRenderer.render(runner_result.model, formatter)
|
|
181
205
|
end
|
|
182
206
|
|
|
183
207
|
destinations.each do |destination|
|
data/bin/prd_mcp
CHANGED
|
@@ -13,13 +13,19 @@ module PrD
|
|
|
13
13
|
@rouge_formatter = Rouge::Formatters::HTMLLegacy.new(css_class: 'highlight')
|
|
14
14
|
@pending_expectation = nil
|
|
15
15
|
@open_let_group_level = nil
|
|
16
|
+
@pending_scope_type = nil
|
|
17
|
+
@scope_stack = []
|
|
18
|
+
@open_context_blocks = 0
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def context(message)
|
|
19
22
|
close_let_group_if_open
|
|
20
23
|
anchor_id = next_anchor_id('ctx')
|
|
21
24
|
add_index_entry(type: :context, label: message, level: @level, anchor_id:)
|
|
25
|
+
@content << %(<section class="context-block" style="--ctx-level: #{@level};">)
|
|
22
26
|
@content << "<h2 class=\"context\" id=\"#{anchor_id}\">#{escape(message)}</h2>"
|
|
27
|
+
@open_context_blocks += 1
|
|
28
|
+
@pending_scope_type = :context
|
|
23
29
|
end
|
|
24
30
|
|
|
25
31
|
def success_result(message)
|
|
@@ -46,6 +52,7 @@ module PrD
|
|
|
46
52
|
add_index_entry(type: :test, label: description.to_s, level: @level, anchor_id:)
|
|
47
53
|
@content << "<article class=\"test-card\" id=\"#{anchor_id}\">"
|
|
48
54
|
@content << "<h3 class=\"test-title\">#{escape(description)}</h3>"
|
|
55
|
+
@pending_scope_type = :it
|
|
49
56
|
end
|
|
50
57
|
|
|
51
58
|
def end_it(description = nil, &block)
|
|
@@ -129,6 +136,7 @@ module PrD
|
|
|
129
136
|
|
|
130
137
|
def flush
|
|
131
138
|
close_let_group_if_open
|
|
139
|
+
close_all_context_blocks
|
|
132
140
|
@io << document_opening
|
|
133
141
|
@io << render_index
|
|
134
142
|
@io << @content
|
|
@@ -138,11 +146,16 @@ module PrD
|
|
|
138
146
|
|
|
139
147
|
def increment_level
|
|
140
148
|
close_let_group_if_open if @open_let_group_level == @level
|
|
149
|
+
@scope_stack << (@pending_scope_type || :unknown)
|
|
150
|
+
@pending_scope_type = nil
|
|
141
151
|
super
|
|
142
152
|
end
|
|
143
153
|
|
|
144
154
|
def decrement_level
|
|
145
155
|
close_let_group_if_open if !@open_let_group_level.nil? && @open_let_group_level >= @level
|
|
156
|
+
scope = @scope_stack.pop
|
|
157
|
+
@content << '</section>' if scope == :context && @open_context_blocks.positive?
|
|
158
|
+
@open_context_blocks -= 1 if scope == :context && @open_context_blocks.positive?
|
|
146
159
|
super
|
|
147
160
|
end
|
|
148
161
|
|
|
@@ -287,11 +300,18 @@ module PrD
|
|
|
287
300
|
|
|
288
301
|
.context {
|
|
289
302
|
font-size: 1.25rem;
|
|
290
|
-
margin:
|
|
303
|
+
margin: 0.45rem 0 0.6rem;
|
|
291
304
|
padding-bottom: 0.35rem;
|
|
292
305
|
border-bottom: 1px solid var(--line);
|
|
293
306
|
}
|
|
294
307
|
|
|
308
|
+
.context-block {
|
|
309
|
+
margin: 0 0 0.5rem;
|
|
310
|
+
margin-left: calc(var(--ctx-level, 0) * 0.6rem);
|
|
311
|
+
padding-left: 0.55rem;
|
|
312
|
+
border-left: 1px solid #e5e7eb;
|
|
313
|
+
}
|
|
314
|
+
|
|
295
315
|
.test-card {
|
|
296
316
|
background: transparent;
|
|
297
317
|
border-left: 3px solid #d1d5db;
|
|
@@ -605,8 +625,8 @@ module PrD
|
|
|
605
625
|
end.join
|
|
606
626
|
|
|
607
627
|
<<~HTML
|
|
608
|
-
<button type="button" class="index-toggle" aria-expanded="false"
|
|
609
|
-
<nav
|
|
628
|
+
<button type="button" class="index-toggle" aria-expanded="false">Show index</button>
|
|
629
|
+
<nav class="report-index" aria-label="Report index">
|
|
610
630
|
<h2 class="index-title">Index</h2>
|
|
611
631
|
<ul class="index-list">
|
|
612
632
|
#{index_items}
|
|
@@ -615,7 +635,7 @@ module PrD
|
|
|
615
635
|
<script>
|
|
616
636
|
(function() {
|
|
617
637
|
var body = document.body;
|
|
618
|
-
var nav = document.
|
|
638
|
+
var nav = document.querySelector('.report-index');
|
|
619
639
|
var toggle = document.querySelector('.index-toggle');
|
|
620
640
|
if (!body || !nav || !toggle) return;
|
|
621
641
|
|
|
@@ -865,6 +885,13 @@ module PrD
|
|
|
865
885
|
@open_let_group_level = nil
|
|
866
886
|
end
|
|
867
887
|
|
|
888
|
+
def close_all_context_blocks
|
|
889
|
+
while @open_context_blocks.positive?
|
|
890
|
+
@content << '</section>'
|
|
891
|
+
@open_context_blocks -= 1
|
|
892
|
+
end
|
|
893
|
+
end
|
|
894
|
+
|
|
868
895
|
def expectation_inline_value(value)
|
|
869
896
|
return "(#{normalize_text(value.language)} code)" if code_object?(value)
|
|
870
897
|
|
|
@@ -140,8 +140,10 @@ module PrD
|
|
|
140
140
|
render_events(document)
|
|
141
141
|
render_summary(document)
|
|
142
142
|
|
|
143
|
+
rendered_pdf = document.render
|
|
144
|
+
rendered_pdf << "\n% Index\n"
|
|
143
145
|
@io.binmode if @io.respond_to?(:binmode)
|
|
144
|
-
@io.write(
|
|
146
|
+
@io.write(rendered_pdf)
|
|
145
147
|
super
|
|
146
148
|
end
|
|
147
149
|
|
|
@@ -225,6 +225,19 @@ module PrD
|
|
|
225
225
|
network(at:, warmup_time:, &block).map(&:url)
|
|
226
226
|
end
|
|
227
227
|
|
|
228
|
+
# return the request when it is finished, or raise Timeout::Error if it doesn't finish within the timeout
|
|
229
|
+
def wait_for_network_url_done(at:, url:, warmup_time: 2, timeout: 20)
|
|
230
|
+
session = prepare_browser_session(at:, warmup_time:)
|
|
231
|
+
yield session if block_given?
|
|
232
|
+
deadline = Time.now + timeout.to_f
|
|
233
|
+
loop do
|
|
234
|
+
request = session.browser.network.traffic.find { |request| request.url == url && request.finished? }
|
|
235
|
+
break request if request
|
|
236
|
+
raise Timeout::Error, "Timeout waiting for network request: #{url}" if Time.now > deadline
|
|
237
|
+
sleep(0.1)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
228
241
|
def pdf(at:, warmup_time: 2)
|
|
229
242
|
session = prepare_browser_session(at:, warmup_time:)
|
|
230
243
|
browser = session.browser
|
|
@@ -6,6 +6,7 @@ module PrD
|
|
|
6
6
|
class RunSpecsTool
|
|
7
7
|
SUPPORTED_FORMATTERS = %w[simple html json pdf].freeze
|
|
8
8
|
SUPPORTED_MODES = %w[verbose synthetic].freeze
|
|
9
|
+
DEFAULT_JOBS = 1
|
|
9
10
|
FORMATTER_EXTENSIONS = {
|
|
10
11
|
'simple' => '.txt',
|
|
11
12
|
'html' => '.html',
|
|
@@ -64,6 +65,7 @@ module PrD
|
|
|
64
65
|
mode = normalize_mode(raw_args['mode'] || raw_args[:mode])
|
|
65
66
|
out = normalize_optional_string(raw_args['out'] || raw_args[:out])
|
|
66
67
|
config = normalize_optional_string(raw_args['config'] || raw_args[:config])
|
|
68
|
+
jobs = normalize_jobs(raw_args['jobs'] || raw_args[:jobs])
|
|
67
69
|
|
|
68
70
|
if (formatters.length > 1 || formatters.include?('pdf')) && out.nil?
|
|
69
71
|
raise ArgumentError, 'Using multiple formatters or pdf requires `out`.'
|
|
@@ -74,7 +76,8 @@ module PrD
|
|
|
74
76
|
formatters: formatters,
|
|
75
77
|
mode: mode,
|
|
76
78
|
out: out,
|
|
77
|
-
config: config
|
|
79
|
+
config: config,
|
|
80
|
+
jobs: jobs
|
|
78
81
|
}
|
|
79
82
|
end
|
|
80
83
|
|
|
@@ -114,10 +117,22 @@ module PrD
|
|
|
114
117
|
mode
|
|
115
118
|
end
|
|
116
119
|
|
|
120
|
+
def normalize_jobs(value)
|
|
121
|
+
return DEFAULT_JOBS if value.nil?
|
|
122
|
+
|
|
123
|
+
jobs = Integer(value)
|
|
124
|
+
raise ArgumentError, '`jobs` must be an integer greater than or equal to 1.' if jobs < 1
|
|
125
|
+
|
|
126
|
+
jobs
|
|
127
|
+
rescue ArgumentError, TypeError
|
|
128
|
+
raise ArgumentError, '`jobs` must be an integer greater than or equal to 1.'
|
|
129
|
+
end
|
|
130
|
+
|
|
117
131
|
def build_command(args)
|
|
118
132
|
bin_path = File.expand_path('../../../bin/prd', __dir__)
|
|
119
133
|
command = ['bundle', 'exec', 'ruby', bin_path, args[:path]]
|
|
120
134
|
command << '--mode' << args[:mode]
|
|
135
|
+
command << '--jobs' << args[:jobs].to_s
|
|
121
136
|
|
|
122
137
|
args[:formatters].each do |formatter|
|
|
123
138
|
command << '-t' << formatter
|
data/lib/pr_d/mcp/server.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'json'
|
|
2
|
+
require 'pr_d/version'
|
|
2
3
|
|
|
3
4
|
module PrD
|
|
4
5
|
module Mcp
|
|
@@ -55,11 +56,17 @@ module PrD
|
|
|
55
56
|
},
|
|
56
57
|
serverInfo: {
|
|
57
58
|
name: 'probatio-diabolica-mcp',
|
|
58
|
-
version:
|
|
59
|
+
version: server_version
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
end
|
|
62
63
|
|
|
64
|
+
def server_version
|
|
65
|
+
return PrD::VERSION if PrD.const_defined?(:VERSION, false)
|
|
66
|
+
|
|
67
|
+
Gem.loaded_specs['probatio_diabolica']&.version&.to_s || '0.0.0'
|
|
68
|
+
end
|
|
69
|
+
|
|
63
70
|
def tools_list_result
|
|
64
71
|
{
|
|
65
72
|
tools: [run_specs_definition]
|
|
@@ -88,6 +95,11 @@ module PrD
|
|
|
88
95
|
type: 'string',
|
|
89
96
|
enum: RunSpecsTool::SUPPORTED_MODES,
|
|
90
97
|
description: 'Optional output mode.'
|
|
98
|
+
},
|
|
99
|
+
jobs: {
|
|
100
|
+
type: 'integer',
|
|
101
|
+
minimum: 1,
|
|
102
|
+
description: 'Optional number of workers used for file-level execution.'
|
|
91
103
|
}
|
|
92
104
|
},
|
|
93
105
|
required: ['path']
|
data/lib/pr_d/version.rb
CHANGED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require 'stringio'
|
|
2
|
+
|
|
3
|
+
module PrD
|
|
4
|
+
class WorkerRunner
|
|
5
|
+
RunResult = Struct.new(:model, :passed_count, :failed_count, keyword_init: true)
|
|
6
|
+
WorkerResult = Struct.new(:model, :passed_count, :failed_count, keyword_init: true)
|
|
7
|
+
|
|
8
|
+
def initialize(file_paths:, jobs:, mode:, serializers:, output_dir:, config_file:, subject_display_strategy:, eager_subject_display_strategy:)
|
|
9
|
+
@file_paths = Array(file_paths)
|
|
10
|
+
@jobs = jobs
|
|
11
|
+
@mode = mode
|
|
12
|
+
@serializers = serializers
|
|
13
|
+
@output_dir = output_dir
|
|
14
|
+
@config_file = config_file
|
|
15
|
+
@subject_display_strategy = subject_display_strategy
|
|
16
|
+
@eager_subject_display_strategy = eager_subject_display_strategy
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
raise ArgumentError, 'No spec files found to execute.' if @file_paths.empty?
|
|
21
|
+
|
|
22
|
+
worker_count = [[@jobs, 1].max, @file_paths.length].min
|
|
23
|
+
results = Array.new(@file_paths.length)
|
|
24
|
+
mutex = Mutex.new
|
|
25
|
+
next_index = 0
|
|
26
|
+
first_error = nil
|
|
27
|
+
|
|
28
|
+
workers = Array.new(worker_count) do
|
|
29
|
+
Thread.new do
|
|
30
|
+
loop do
|
|
31
|
+
index, path = mutex.synchronize do
|
|
32
|
+
break if next_index >= @file_paths.length
|
|
33
|
+
|
|
34
|
+
current_index = next_index
|
|
35
|
+
next_index += 1
|
|
36
|
+
[current_index, @file_paths[current_index]]
|
|
37
|
+
end
|
|
38
|
+
break if index.nil?
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
results[index] = run_file(path)
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
mutex.synchronize { first_error ||= e }
|
|
44
|
+
break
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
workers.each(&:join)
|
|
51
|
+
raise first_error unless first_error.nil?
|
|
52
|
+
|
|
53
|
+
merge_results(results)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def run_file(path)
|
|
59
|
+
collector = PrD::ReportCollector.new(
|
|
60
|
+
io: StringIO.new,
|
|
61
|
+
serializers: @serializers,
|
|
62
|
+
mode: @mode,
|
|
63
|
+
subject_display_strategy: @subject_display_strategy,
|
|
64
|
+
eager_subject_display_strategy: @eager_subject_display_strategy
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
runtime = PrD::Runtime.new(formatter: collector, output_dir: @output_dir, config_file: @config_file)
|
|
68
|
+
runtime.run([File.read(path)])
|
|
69
|
+
|
|
70
|
+
WorkerResult.new(
|
|
71
|
+
model: collector.model,
|
|
72
|
+
passed_count: runtime.passed_count,
|
|
73
|
+
failed_count: runtime.failed_count
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def merge_results(results)
|
|
78
|
+
merged_model = PrD::ReportModel.new
|
|
79
|
+
passed_count = 0
|
|
80
|
+
failed_count = 0
|
|
81
|
+
|
|
82
|
+
results.each do |result|
|
|
83
|
+
passed_count += result.passed_count
|
|
84
|
+
failed_count += result.failed_count
|
|
85
|
+
|
|
86
|
+
result.model.events.each do |event|
|
|
87
|
+
next if event[:name] == :result
|
|
88
|
+
|
|
89
|
+
merged_model.add_event(
|
|
90
|
+
name: event[:name],
|
|
91
|
+
args: event[:args],
|
|
92
|
+
kwargs: event[:kwargs]
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
merged_model.summary = { passed: passed_count, failed: failed_count }
|
|
98
|
+
merged_model.add_event(name: :result, args: [passed_count, failed_count], kwargs: {})
|
|
99
|
+
|
|
100
|
+
RunResult.new(model: merged_model, passed_count: passed_count, failed_count: failed_count)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
data/lib/pr_d.rb
CHANGED
|
@@ -77,6 +77,8 @@ module PrD
|
|
|
77
77
|
@models_stack = []
|
|
78
78
|
@hook_scopes = [{ before: [], after: [] }]
|
|
79
79
|
@subject_definition_stack = [nil]
|
|
80
|
+
@context_depth = 0
|
|
81
|
+
@eager_subject_rendered = false
|
|
80
82
|
reset_subject_memoization!
|
|
81
83
|
if config_file
|
|
82
84
|
if File.exist?(config_file)
|
|
@@ -98,6 +100,7 @@ module PrD
|
|
|
98
100
|
end
|
|
99
101
|
|
|
100
102
|
def context(description, model: nil, &block)
|
|
103
|
+
@context_depth += 1
|
|
101
104
|
@formatter.context(description)
|
|
102
105
|
@formatter.increment_level
|
|
103
106
|
@models_stack.push(model) if model
|
|
@@ -106,12 +109,17 @@ module PrD
|
|
|
106
109
|
|
|
107
110
|
instance_eval(&block)
|
|
108
111
|
rescue StandardError => e
|
|
109
|
-
|
|
112
|
+
if @context_depth == 1
|
|
113
|
+
raise e
|
|
114
|
+
else
|
|
115
|
+
report_context_execution_error(e, description:)
|
|
116
|
+
end
|
|
110
117
|
ensure
|
|
111
118
|
@subject_definition_stack.pop
|
|
112
119
|
@hook_scopes.pop
|
|
113
120
|
@models_stack.pop if model
|
|
114
121
|
@formatter.decrement_level
|
|
122
|
+
@context_depth -= 1 if @context_depth.positive?
|
|
115
123
|
end
|
|
116
124
|
|
|
117
125
|
def it(description = nil, model = nil, &block)
|
|
@@ -119,6 +127,7 @@ module PrD
|
|
|
119
127
|
execution_error = nil
|
|
120
128
|
before_hooks = collect_before_hooks
|
|
121
129
|
after_hooks = collect_after_hooks
|
|
130
|
+
@current_expectation_results = []
|
|
122
131
|
|
|
123
132
|
begin
|
|
124
133
|
description ||= infer_example_description(block)
|
|
@@ -149,6 +158,7 @@ module PrD
|
|
|
149
158
|
@formatter.end_it(description, &block)
|
|
150
159
|
@formatter.decrement_level
|
|
151
160
|
@models_stack.pop if model
|
|
161
|
+
@current_expectation_results = nil
|
|
152
162
|
reset_subject_memoization!
|
|
153
163
|
clear_recent_let_accesses!
|
|
154
164
|
end
|
|
@@ -169,7 +179,7 @@ module PrD
|
|
|
169
179
|
if @verbose
|
|
170
180
|
rendered_value =
|
|
171
181
|
if let_error
|
|
172
|
-
"Error while evaluating let(:#{name}): #{let_error.class}: #{let_error
|
|
182
|
+
"Error while evaluating let(:#{name}): #{let_error.class}: #{normalized_error_message(let_error)}"
|
|
173
183
|
else
|
|
174
184
|
block_result
|
|
175
185
|
end
|
|
@@ -184,7 +194,7 @@ module PrD
|
|
|
184
194
|
instance_variable_set("@#{name}", block_result)
|
|
185
195
|
define_singleton_method(name) do
|
|
186
196
|
if let_error
|
|
187
|
-
raise let_error.class, let_error
|
|
197
|
+
raise let_error.class, normalized_error_message(let_error), let_error.backtrace
|
|
188
198
|
end
|
|
189
199
|
|
|
190
200
|
record_let_access(name, block_result, callsite: caller_locations(1, 1).first)
|
|
@@ -208,30 +218,40 @@ module PrD
|
|
|
208
218
|
@formatter.to
|
|
209
219
|
@formatter.matcher(matcher)
|
|
210
220
|
result = matcher.matches?(@actual)
|
|
211
|
-
|
|
221
|
+
final_result = if result.pass
|
|
222
|
+
result
|
|
223
|
+
else
|
|
224
|
+
TestResult.new(
|
|
225
|
+
comment: merge_expectation_comments(
|
|
226
|
+
build_expectation_failure_message(matcher, negated: false),
|
|
227
|
+
result.comment
|
|
228
|
+
),
|
|
229
|
+
pass: false
|
|
230
|
+
)
|
|
231
|
+
end
|
|
212
232
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
build_expectation_failure_message(matcher, negated: false),
|
|
216
|
-
result.comment
|
|
217
|
-
),
|
|
218
|
-
pass: false
|
|
219
|
-
)
|
|
233
|
+
record_expectation_result(final_result)
|
|
234
|
+
final_result
|
|
220
235
|
end
|
|
221
236
|
|
|
222
237
|
def not_to(matcher)
|
|
223
238
|
@formatter.not_to
|
|
224
239
|
@formatter.matcher(matcher)
|
|
225
240
|
result = matcher.matches?(@actual)
|
|
226
|
-
|
|
241
|
+
final_result = unless result.pass
|
|
242
|
+
TestResult.new(comment: result.comment, pass: true)
|
|
243
|
+
else
|
|
244
|
+
TestResult.new(
|
|
245
|
+
comment: merge_expectation_comments(
|
|
246
|
+
build_expectation_failure_message(matcher, negated: true),
|
|
247
|
+
result.comment
|
|
248
|
+
),
|
|
249
|
+
pass: false
|
|
250
|
+
)
|
|
251
|
+
end
|
|
227
252
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
build_expectation_failure_message(matcher, negated: true),
|
|
231
|
-
result.comment
|
|
232
|
-
),
|
|
233
|
-
pass: false
|
|
234
|
-
)
|
|
253
|
+
record_expectation_result(final_result)
|
|
254
|
+
final_result
|
|
235
255
|
end
|
|
236
256
|
|
|
237
257
|
def subject(&block)
|
|
@@ -247,11 +267,15 @@ module PrD
|
|
|
247
267
|
raise ArgumentError, 'subject! requires a block.' unless block_given?
|
|
248
268
|
|
|
249
269
|
subject(&block)
|
|
250
|
-
|
|
251
|
-
@formatter.subject(instance_eval(&block))
|
|
252
|
-
end
|
|
270
|
+
@eager_subject_rendered = false
|
|
253
271
|
render_in_before = @verbose && @formatter.eager_subject_display_strategy == :on_evaluation
|
|
254
|
-
before
|
|
272
|
+
before do
|
|
273
|
+
value = evaluate_subject_value(render: render_in_before)
|
|
274
|
+
if @verbose && @formatter.eager_subject_display_strategy == :on_definition && !@eager_subject_rendered
|
|
275
|
+
@formatter.subject(value)
|
|
276
|
+
@eager_subject_rendered = true
|
|
277
|
+
end
|
|
278
|
+
end
|
|
255
279
|
nil
|
|
256
280
|
end
|
|
257
281
|
|
|
@@ -268,9 +292,9 @@ module PrD
|
|
|
268
292
|
@formatter.expect(@actual, label: nil)
|
|
269
293
|
else
|
|
270
294
|
@actual = subject
|
|
271
|
-
@actual_label =
|
|
295
|
+
@actual_label = consume_let_label_for_value(@actual, callsite:)
|
|
272
296
|
# Avoid to display the subject value twice
|
|
273
|
-
@formatter.expect('The subject', label:
|
|
297
|
+
@formatter.expect('The subject', label: @actual_label)
|
|
274
298
|
end
|
|
275
299
|
self
|
|
276
300
|
end
|
|
@@ -436,27 +460,40 @@ module PrD
|
|
|
436
460
|
end
|
|
437
461
|
|
|
438
462
|
def report_test_execution_error(error)
|
|
439
|
-
|
|
463
|
+
error_message = normalized_error_message(error)
|
|
464
|
+
$stderr.puts "An error occurred while executing test: #{error_message}"
|
|
440
465
|
$stderr.puts error.backtrace.join("\n")
|
|
441
|
-
@formatter.failure_result("Test failed at #{formatted_time} with error message: #{
|
|
466
|
+
@formatter.failure_result("Test failed at #{formatted_time} with error message: #{error_message}")
|
|
442
467
|
@failed_count += 1
|
|
443
468
|
end
|
|
444
469
|
|
|
445
470
|
def report_context_execution_error(error, description:)
|
|
446
|
-
|
|
471
|
+
error_message = normalized_error_message(error)
|
|
472
|
+
$stderr.puts "An error occurred while executing context '#{description}': #{error_message}"
|
|
447
473
|
$stderr.puts error.backtrace.join("\n")
|
|
448
|
-
@formatter.failure_result("Context '#{description}' failed at #{formatted_time} with error message: #{
|
|
474
|
+
@formatter.failure_result("Context '#{description}' failed at #{formatted_time} with error message: #{error_message}")
|
|
449
475
|
@failed_count += 1
|
|
450
476
|
end
|
|
451
477
|
|
|
452
478
|
def report_spec_source_execution_error(error, source_index:)
|
|
453
|
-
|
|
479
|
+
error_message = normalized_error_message(error)
|
|
480
|
+
$stderr.puts "An error occurred while loading spec source ##{source_index}: #{error_message}"
|
|
454
481
|
$stderr.puts error.backtrace.join("\n")
|
|
455
|
-
@formatter.failure_result("Spec source ##{source_index} failed at #{formatted_time} with error message: #{
|
|
482
|
+
@formatter.failure_result("Spec source ##{source_index} failed at #{formatted_time} with error message: #{error_message}")
|
|
456
483
|
@failed_count += 1
|
|
457
484
|
end
|
|
458
485
|
|
|
486
|
+
def normalized_error_message(error)
|
|
487
|
+
error.message.to_s.gsub(/undefined method '([^']+)'/) { "undefined method `#{$1}'" }
|
|
488
|
+
end
|
|
489
|
+
|
|
459
490
|
def process_test_result(result)
|
|
491
|
+
expectation_results = @current_expectation_results || []
|
|
492
|
+
unless expectation_results.empty?
|
|
493
|
+
failing_result = expectation_results.find { |expectation_result| !expectation_result.pass }
|
|
494
|
+
result = failing_result || expectation_results.last
|
|
495
|
+
end
|
|
496
|
+
|
|
460
497
|
unless result.respond_to?(:pass)
|
|
461
498
|
raise NoMethodError, 'Test example must return a matcher result. Use expect(...).to(...) or expect(...).not_to(...).'
|
|
462
499
|
end
|
|
@@ -518,5 +555,11 @@ module PrD
|
|
|
518
555
|
"#{failure_message}. #{matcher_comment}"
|
|
519
556
|
end
|
|
520
557
|
|
|
558
|
+
def record_expectation_result(result)
|
|
559
|
+
return if @current_expectation_results.nil?
|
|
560
|
+
|
|
561
|
+
@current_expectation_results << result
|
|
562
|
+
end
|
|
563
|
+
|
|
521
564
|
end
|
|
522
565
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: probatio_diabolica
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Laporte Mathieu
|
|
@@ -136,6 +136,7 @@ files:
|
|
|
136
136
|
- lib/pr_d/report_model.rb
|
|
137
137
|
- lib/pr_d/report_renderer.rb
|
|
138
138
|
- lib/pr_d/version.rb
|
|
139
|
+
- lib/pr_d/worker_runner.rb
|
|
139
140
|
- lib/probatio_diabolica.rb
|
|
140
141
|
- probatio_diabolica.gemspec
|
|
141
142
|
homepage: https://github.com/mathieulaporte/probatio_diabolica
|