probatio_diabolica 0.4.3 → 0.4.4
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/bin/prd +31 -7
- data/lib/pr_d/formatters/formatter.rb +14 -0
- data/lib/pr_d/formatters/html_formatter.rb +166 -39
- data/lib/pr_d/report_collector.rb +132 -0
- data/lib/pr_d/report_model.rb +181 -0
- data/lib/pr_d/report_renderer.rb +20 -0
- data/lib/pr_d/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 110c77befc1ae2a7b81ade0e355482bc56ff3a69d1a53dea7a6684a126f84643
|
|
4
|
+
data.tar.gz: e68915cc2e7e7f81e73047c8d833011e62fbc44352e8308f70c34f02b516fee4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1ce65d410be41063de1b4ca71d1cfe9f78396626c869bfda485d6bac4c26f16420e60f0b6fa6e39d4525accb581169271188c3545f2de1fb61f28f5ae3aecdc4
|
|
7
|
+
data.tar.gz: ff5ae5a71121442c5f210c50693250480839a47c78229a9b1b7ae91e23bbcb9f209b42d44d4516dd802d67a45cc9b144c2e5f6fc5cc4338ae4844a05f335f57d
|
data/bin/prd
CHANGED
|
@@ -7,6 +7,7 @@ rescue LoadError
|
|
|
7
7
|
end
|
|
8
8
|
require 'optparse'
|
|
9
9
|
require 'fileutils'
|
|
10
|
+
require 'stringio'
|
|
10
11
|
|
|
11
12
|
FORMATTERS = {
|
|
12
13
|
'simple' => { klass: PrD::Formatters::SimpleFormatter, extension: '.txt', binary: false },
|
|
@@ -133,7 +134,7 @@ serializers = {
|
|
|
133
134
|
|
|
134
135
|
destinations =
|
|
135
136
|
if out_base.nil?
|
|
136
|
-
[{ io: STDOUT,
|
|
137
|
+
[{ io: STDOUT, formatter_type: formatter_types.first }]
|
|
137
138
|
else
|
|
138
139
|
output_dir = File.dirname(out_base)
|
|
139
140
|
FileUtils.mkdir_p(output_dir)
|
|
@@ -141,21 +142,44 @@ destinations =
|
|
|
141
142
|
config = FORMATTERS.fetch(type)
|
|
142
143
|
output_path = "#{out_base}#{config[:extension]}"
|
|
143
144
|
io = File.open(output_path, config[:binary] ? 'wb' : 'w')
|
|
144
|
-
{ io:,
|
|
145
|
+
{ io:, formatter_type: type }
|
|
145
146
|
end
|
|
146
147
|
end
|
|
147
148
|
|
|
148
|
-
|
|
149
|
-
if
|
|
150
|
-
|
|
149
|
+
subject_display_strategy =
|
|
150
|
+
if formatter_types.length == 1
|
|
151
|
+
FORMATTERS.fetch(formatter_types.first)[:klass].new(io: StringIO.new, serializers:, mode: mode.to_sym).subject_display_strategy
|
|
151
152
|
else
|
|
152
|
-
|
|
153
|
+
nil
|
|
153
154
|
end
|
|
154
155
|
|
|
156
|
+
eager_subject_display_strategy =
|
|
157
|
+
if formatter_types.length == 1
|
|
158
|
+
FORMATTERS.fetch(formatter_types.first)[:klass].new(io: StringIO.new, serializers:, mode: mode.to_sym).eager_subject_display_strategy
|
|
159
|
+
else
|
|
160
|
+
nil
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
collector = PrD::ReportCollector.new(
|
|
164
|
+
io: StringIO.new,
|
|
165
|
+
serializers: serializers,
|
|
166
|
+
mode: mode.to_sym,
|
|
167
|
+
subject_display_strategy: subject_display_strategy,
|
|
168
|
+
eager_subject_display_strategy: eager_subject_display_strategy
|
|
169
|
+
)
|
|
170
|
+
|
|
155
171
|
runtime_output_dir = out_base.nil? ? nil : File.dirname(out_base)
|
|
156
|
-
runtime = PrD::Runtime.new(formatter
|
|
172
|
+
runtime = PrD::Runtime.new(formatter: collector, output_dir: runtime_output_dir, config_file: options[:config])
|
|
157
173
|
runtime.run(tests)
|
|
158
174
|
exit_status = runtime.success? ? 0 : 1
|
|
175
|
+
|
|
176
|
+
destinations.each do |destination|
|
|
177
|
+
io = destination[:io]
|
|
178
|
+
type = destination[:formatter_type]
|
|
179
|
+
formatter = FORMATTERS.fetch(type)[:klass].new(io:, serializers:, mode: mode.to_sym)
|
|
180
|
+
PrD::ReportRenderer.render(collector.model, formatter)
|
|
181
|
+
end
|
|
182
|
+
|
|
159
183
|
destinations.each do |destination|
|
|
160
184
|
io = destination[:io]
|
|
161
185
|
io.close if io != STDOUT
|
|
@@ -117,6 +117,7 @@ module PrD
|
|
|
117
117
|
serializer = @serializers[value.class]
|
|
118
118
|
return serializer.call(value) if serializer
|
|
119
119
|
return ferrum_node_summary(value) if ferrum_node?(value)
|
|
120
|
+
return value.path if value.respond_to?(:path) && value.path.is_a?(String) && !value.path.empty?
|
|
120
121
|
return value.path if value.is_a?(File)
|
|
121
122
|
return value.map { |v| serialize(v) } if value.is_a?(Array)
|
|
122
123
|
return value.transform_values { |v| serialize(v) } if value.is_a?(Hash)
|
|
@@ -129,12 +130,25 @@ module PrD
|
|
|
129
130
|
end
|
|
130
131
|
|
|
131
132
|
def ferrum_node?(value)
|
|
133
|
+
return true if defined?(PrD::ReportModel::FerrumNodeSnapshot) && value.is_a?(PrD::ReportModel::FerrumNodeSnapshot)
|
|
134
|
+
|
|
132
135
|
value.respond_to?(:class) && value.class.respond_to?(:name) && value.class.name == 'Ferrum::Node'
|
|
133
136
|
rescue StandardError
|
|
134
137
|
false
|
|
135
138
|
end
|
|
136
139
|
|
|
137
140
|
def ferrum_node_payload(node)
|
|
141
|
+
if defined?(PrD::ReportModel::FerrumNodeSnapshot) && node.is_a?(PrD::ReportModel::FerrumNodeSnapshot)
|
|
142
|
+
payload = node.payload || {}
|
|
143
|
+
payload[:tag] = payload[:tag].to_s.downcase.strip unless blank_text?(payload[:tag])
|
|
144
|
+
payload[:id] = payload[:id].to_s.strip unless blank_text?(payload[:id])
|
|
145
|
+
payload[:classes] = normalize_classes(payload[:classes])
|
|
146
|
+
payload[:text] = normalize_preview_text(payload[:text], max_length: 160)
|
|
147
|
+
payload[:html] = normalize_preview_text(payload[:html], max_length: 220)
|
|
148
|
+
payload[:description] = normalize_preview_text(payload[:description], max_length: 160)
|
|
149
|
+
return payload
|
|
150
|
+
end
|
|
151
|
+
|
|
138
152
|
payload = ferrum_node_payload_from_js(node) || {}
|
|
139
153
|
payload[:tag] = payload[:tag].to_s.downcase.strip unless blank_text?(payload[:tag])
|
|
140
154
|
payload[:id] = payload[:id].to_s.strip unless blank_text?(payload[:id])
|
|
@@ -12,9 +12,11 @@ module PrD
|
|
|
12
12
|
@anchor_counters = Hash.new(0)
|
|
13
13
|
@rouge_formatter = Rouge::Formatters::HTMLLegacy.new(css_class: 'highlight')
|
|
14
14
|
@pending_expectation = nil
|
|
15
|
+
@open_let_group_level = nil
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def context(message)
|
|
19
|
+
close_let_group_if_open
|
|
18
20
|
anchor_id = next_anchor_id('ctx')
|
|
19
21
|
add_index_entry(type: :context, label: message, level: @level, anchor_id:)
|
|
20
22
|
@content << "<h2 class=\"context\" id=\"#{anchor_id}\">#{escape(message)}</h2>"
|
|
@@ -37,6 +39,7 @@ module PrD
|
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
def it(description = nil, &block)
|
|
42
|
+
close_let_group_if_open
|
|
40
43
|
@current_test_title = description.to_s
|
|
41
44
|
@pending_expectation = nil
|
|
42
45
|
anchor_id = next_anchor_id('test')
|
|
@@ -56,6 +59,7 @@ module PrD
|
|
|
56
59
|
|
|
57
60
|
def let(name_or_value, value = MISSING_VALUE)
|
|
58
61
|
return if synthetic?
|
|
62
|
+
start_let_group_if_needed
|
|
59
63
|
name, rendered_value = named_value_arguments(name_or_value, value)
|
|
60
64
|
label = name.nil? ? 'Let' : "Let(:#{name})"
|
|
61
65
|
render_collapsible_let_block(label, rendered_value)
|
|
@@ -63,6 +67,7 @@ module PrD
|
|
|
63
67
|
|
|
64
68
|
def subject(subject)
|
|
65
69
|
return if synthetic?
|
|
70
|
+
close_let_group_if_open
|
|
66
71
|
render_value_block('Subject', subject)
|
|
67
72
|
end
|
|
68
73
|
|
|
@@ -75,6 +80,7 @@ module PrD
|
|
|
75
80
|
end
|
|
76
81
|
|
|
77
82
|
def pending(description = nil)
|
|
83
|
+
close_let_group_if_open
|
|
78
84
|
pending_label = description || 'Pending test'
|
|
79
85
|
anchor_id = next_anchor_id('pending')
|
|
80
86
|
add_index_entry(type: :pending, label: pending_label, level: @level, anchor_id:)
|
|
@@ -122,6 +128,7 @@ module PrD
|
|
|
122
128
|
end
|
|
123
129
|
|
|
124
130
|
def flush
|
|
131
|
+
close_let_group_if_open
|
|
125
132
|
@io << document_opening
|
|
126
133
|
@io << render_index
|
|
127
134
|
@io << @content
|
|
@@ -129,6 +136,16 @@ module PrD
|
|
|
129
136
|
super
|
|
130
137
|
end
|
|
131
138
|
|
|
139
|
+
def increment_level
|
|
140
|
+
close_let_group_if_open if @open_let_group_level == @level
|
|
141
|
+
super
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def decrement_level
|
|
145
|
+
close_let_group_if_open if !@open_let_group_level.nil? && @open_let_group_level >= @level
|
|
146
|
+
super
|
|
147
|
+
end
|
|
148
|
+
|
|
132
149
|
private
|
|
133
150
|
|
|
134
151
|
def document_opening
|
|
@@ -168,7 +185,6 @@ module PrD
|
|
|
168
185
|
margin: 1rem auto 2rem;
|
|
169
186
|
padding: 1rem;
|
|
170
187
|
background: var(--paper);
|
|
171
|
-
border: 1px solid var(--line);
|
|
172
188
|
border-radius: 4px;
|
|
173
189
|
}
|
|
174
190
|
|
|
@@ -205,7 +221,6 @@ module PrD
|
|
|
205
221
|
z-index: 1000;
|
|
206
222
|
width: var(--sidebar-width);
|
|
207
223
|
background: var(--paper);
|
|
208
|
-
border: 1px solid var(--line);
|
|
209
224
|
border-radius: 4px;
|
|
210
225
|
padding: 0.9rem 1rem;
|
|
211
226
|
overflow-y: auto;
|
|
@@ -234,8 +249,31 @@ module PrD
|
|
|
234
249
|
padding-left: calc(var(--index-level, 0) * 1rem);
|
|
235
250
|
}
|
|
236
251
|
|
|
252
|
+
.index-row {
|
|
253
|
+
display: flex;
|
|
254
|
+
align-items: center;
|
|
255
|
+
gap: 0.3rem;
|
|
256
|
+
min-width: 0;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.context-toggle {
|
|
260
|
+
border: 1px solid var(--line);
|
|
261
|
+
border-radius: 3px;
|
|
262
|
+
background: #fff;
|
|
263
|
+
color: var(--accent);
|
|
264
|
+
cursor: pointer;
|
|
265
|
+
font-size: 0.72rem;
|
|
266
|
+
width: 1.2rem;
|
|
267
|
+
height: 1.2rem;
|
|
268
|
+
line-height: 1;
|
|
269
|
+
padding: 0;
|
|
270
|
+
flex: 0 0 auto;
|
|
271
|
+
}
|
|
272
|
+
|
|
237
273
|
.index-link {
|
|
238
274
|
display: block;
|
|
275
|
+
flex: 1 1 auto;
|
|
276
|
+
min-width: 0;
|
|
239
277
|
white-space: nowrap;
|
|
240
278
|
overflow: hidden;
|
|
241
279
|
text-overflow: ellipsis;
|
|
@@ -255,15 +293,26 @@ module PrD
|
|
|
255
293
|
}
|
|
256
294
|
|
|
257
295
|
.test-card {
|
|
258
|
-
background:
|
|
259
|
-
border:
|
|
260
|
-
|
|
261
|
-
padding: 0.9rem 1rem;
|
|
296
|
+
background: transparent;
|
|
297
|
+
border-left: 3px solid #d1d5db;
|
|
298
|
+
padding: 0.8rem 0.95rem;
|
|
262
299
|
margin: 0 0 1rem;
|
|
263
300
|
}
|
|
264
301
|
|
|
302
|
+
.test-card:has(.status.success) {
|
|
303
|
+
border-left-color: #16a34a;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.test-card:has(.status.failure) {
|
|
307
|
+
border-left-color: #dc2626;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.test-card:has(.status.pending) {
|
|
311
|
+
border-left-color: #d97706;
|
|
312
|
+
}
|
|
313
|
+
|
|
265
314
|
.test-title {
|
|
266
|
-
margin: 0 0 0.
|
|
315
|
+
margin: 0 0 0.5rem;
|
|
267
316
|
font-size: 1.1rem;
|
|
268
317
|
}
|
|
269
318
|
|
|
@@ -315,53 +364,65 @@ module PrD
|
|
|
315
364
|
}
|
|
316
365
|
|
|
317
366
|
.status {
|
|
318
|
-
margin: 0.
|
|
319
|
-
padding: 0
|
|
320
|
-
border
|
|
321
|
-
border:
|
|
322
|
-
font-weight:
|
|
367
|
+
margin: 0.22rem 0 0;
|
|
368
|
+
padding: 0;
|
|
369
|
+
border: 0;
|
|
370
|
+
border-radius: 0;
|
|
371
|
+
font-weight: 400;
|
|
372
|
+
font-size: 0.76rem;
|
|
373
|
+
letter-spacing: 0.01em;
|
|
374
|
+
color: var(--muted);
|
|
375
|
+
opacity: 0.8;
|
|
323
376
|
}
|
|
324
377
|
|
|
325
378
|
.status.success {
|
|
326
|
-
background:
|
|
327
|
-
color: var(--
|
|
379
|
+
background: transparent;
|
|
380
|
+
color: var(--muted);
|
|
328
381
|
}
|
|
329
382
|
|
|
330
383
|
.status.failure {
|
|
331
|
-
background:
|
|
332
|
-
color: var(--
|
|
384
|
+
background: transparent;
|
|
385
|
+
color: var(--muted);
|
|
333
386
|
}
|
|
334
387
|
|
|
335
388
|
.status.pending {
|
|
336
|
-
background:
|
|
337
|
-
color: var(--
|
|
389
|
+
background: transparent;
|
|
390
|
+
color: var(--muted);
|
|
338
391
|
}
|
|
339
392
|
|
|
340
393
|
.subject-block {
|
|
341
|
-
margin: 0.
|
|
342
|
-
padding: 0.
|
|
394
|
+
margin: 0.18rem 0 0.34rem;
|
|
395
|
+
padding: 0.36rem 0.5rem;
|
|
343
396
|
border: 1px solid #d1d5db;
|
|
344
397
|
border-radius: 4px;
|
|
345
|
-
background: #
|
|
398
|
+
background: #fcfcfc;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.lets-group {
|
|
402
|
+
margin: 0.2rem 0 0.6rem;
|
|
403
|
+
padding: 0.18rem 0.38rem;
|
|
404
|
+
background: #fcfcfc;
|
|
405
|
+
border-left: 2px solid #e5e7eb;
|
|
406
|
+
border-radius: 3px;
|
|
346
407
|
}
|
|
347
408
|
|
|
348
409
|
.let-block {
|
|
349
|
-
margin: 0.
|
|
350
|
-
border:
|
|
351
|
-
border-radius:
|
|
352
|
-
background:
|
|
410
|
+
margin: 0.15rem 0;
|
|
411
|
+
border: 0;
|
|
412
|
+
border-radius: 0;
|
|
413
|
+
background: transparent;
|
|
353
414
|
}
|
|
354
415
|
|
|
355
416
|
.let-toggle {
|
|
356
417
|
list-style: none;
|
|
357
418
|
cursor: pointer;
|
|
358
|
-
padding: 0.
|
|
419
|
+
padding: 0.24rem 0.26rem;
|
|
359
420
|
display: flex;
|
|
360
421
|
align-items: center;
|
|
361
422
|
justify-content: space-between;
|
|
362
|
-
gap: 0.
|
|
363
|
-
font-size: 0.
|
|
364
|
-
font-weight:
|
|
423
|
+
gap: 0.4rem;
|
|
424
|
+
font-size: 0.82rem;
|
|
425
|
+
font-weight: 500;
|
|
365
426
|
color: #0f172a;
|
|
366
427
|
}
|
|
367
428
|
|
|
@@ -372,7 +433,7 @@ module PrD
|
|
|
372
433
|
.let-toggle::after {
|
|
373
434
|
content: 'Open';
|
|
374
435
|
color: var(--muted);
|
|
375
|
-
font-size: 0.
|
|
436
|
+
font-size: 0.68rem;
|
|
376
437
|
letter-spacing: 0.03em;
|
|
377
438
|
text-transform: uppercase;
|
|
378
439
|
}
|
|
@@ -382,7 +443,7 @@ module PrD
|
|
|
382
443
|
}
|
|
383
444
|
|
|
384
445
|
.let-content {
|
|
385
|
-
padding: 0 0.
|
|
446
|
+
padding: 0 0.28rem 0.12rem;
|
|
386
447
|
}
|
|
387
448
|
|
|
388
449
|
.let-content .subject-block {
|
|
@@ -526,7 +587,21 @@ module PrD
|
|
|
526
587
|
|
|
527
588
|
index_items = @index_entries.map do |entry|
|
|
528
589
|
label = escape(index_label(entry))
|
|
529
|
-
|
|
590
|
+
type = entry[:type].to_s
|
|
591
|
+
toggle =
|
|
592
|
+
if entry[:type] == :context
|
|
593
|
+
'<button type="button" class="context-toggle" aria-expanded="false" title="Unfold context">+</button>'
|
|
594
|
+
else
|
|
595
|
+
''
|
|
596
|
+
end
|
|
597
|
+
<<~HTML.chomp
|
|
598
|
+
<li class="index-item" style="--index-level: #{entry[:level]};" data-level="#{entry[:level]}" data-type="#{type}">
|
|
599
|
+
<div class="index-row">
|
|
600
|
+
#{toggle}
|
|
601
|
+
<a class="index-link" href="##{entry[:anchor_id]}" title="#{label}">#{label}</a>
|
|
602
|
+
</div>
|
|
603
|
+
</li>
|
|
604
|
+
HTML
|
|
530
605
|
end.join
|
|
531
606
|
|
|
532
607
|
<<~HTML
|
|
@@ -557,6 +632,49 @@ module PrD
|
|
|
557
632
|
syncToggleLabel();
|
|
558
633
|
});
|
|
559
634
|
|
|
635
|
+
var items = Array.prototype.slice.call(nav.querySelectorAll('.index-item'));
|
|
636
|
+
var contextButtons = Array.prototype.slice.call(nav.querySelectorAll('.context-toggle'));
|
|
637
|
+
|
|
638
|
+
var levelOf = function(item) {
|
|
639
|
+
return Number(item.getAttribute('data-level') || '0');
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
var descendantsOf = function(parentItem) {
|
|
643
|
+
var start = items.indexOf(parentItem);
|
|
644
|
+
if (start < 0) return [];
|
|
645
|
+
var parentLevel = levelOf(parentItem);
|
|
646
|
+
var descendants = [];
|
|
647
|
+
|
|
648
|
+
for (var i = start + 1; i < items.length; i += 1) {
|
|
649
|
+
var candidate = items[i];
|
|
650
|
+
if (levelOf(candidate) <= parentLevel) break;
|
|
651
|
+
descendants.push(candidate);
|
|
652
|
+
}
|
|
653
|
+
return descendants;
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
contextButtons.forEach(function(button) {
|
|
657
|
+
var contextItem = button.closest('.index-item');
|
|
658
|
+
if (!contextItem) return;
|
|
659
|
+
|
|
660
|
+
button.addEventListener('click', function(event) {
|
|
661
|
+
event.preventDefault();
|
|
662
|
+
var expanded = button.getAttribute('aria-expanded') !== 'false';
|
|
663
|
+
var nextExpanded = !expanded;
|
|
664
|
+
button.setAttribute('aria-expanded', String(nextExpanded));
|
|
665
|
+
button.textContent = nextExpanded ? '-' : '+';
|
|
666
|
+
button.title = nextExpanded ? 'Fold context' : 'Unfold context';
|
|
667
|
+
|
|
668
|
+
descendantsOf(contextItem).forEach(function(item) {
|
|
669
|
+
item.style.display = nextExpanded ? '' : 'none';
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
descendantsOf(contextItem).forEach(function(item) {
|
|
674
|
+
item.style.display = 'none';
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
|
|
560
678
|
syncToggleLabel();
|
|
561
679
|
})();
|
|
562
680
|
</script>
|
|
@@ -564,13 +682,7 @@ module PrD
|
|
|
564
682
|
end
|
|
565
683
|
|
|
566
684
|
def index_label(entry)
|
|
567
|
-
|
|
568
|
-
case entry[:type]
|
|
569
|
-
when :context then '+'
|
|
570
|
-
else '-'
|
|
571
|
-
end
|
|
572
|
-
|
|
573
|
-
"#{marker} #{entry[:label]}"
|
|
685
|
+
"#{entry[:label]}"
|
|
574
686
|
end
|
|
575
687
|
|
|
576
688
|
def add_index_entry(type:, label:, level:, anchor_id:)
|
|
@@ -738,6 +850,21 @@ module PrD
|
|
|
738
850
|
render_expectation_code_block('Expected', expected_value) if expected_present
|
|
739
851
|
end
|
|
740
852
|
|
|
853
|
+
def start_let_group_if_needed
|
|
854
|
+
return if @open_let_group_level == @level
|
|
855
|
+
|
|
856
|
+
close_let_group_if_open
|
|
857
|
+
@content << '<section class="lets-group">'
|
|
858
|
+
@open_let_group_level = @level
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
def close_let_group_if_open
|
|
862
|
+
return if @open_let_group_level.nil?
|
|
863
|
+
|
|
864
|
+
@content << '</section>'
|
|
865
|
+
@open_let_group_level = nil
|
|
866
|
+
end
|
|
867
|
+
|
|
741
868
|
def expectation_inline_value(value)
|
|
742
869
|
return "(#{normalize_text(value.language)} code)" if code_object?(value)
|
|
743
870
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
module PrD
|
|
2
|
+
class ReportCollector < PrD::Formatters::Formatter
|
|
3
|
+
def initialize(io: $stdout, serializers: {}, mode: :verbose, display_adapters: {}, subject_display_strategy: :on_evaluation, eager_subject_display_strategy: :on_evaluation)
|
|
4
|
+
super(io: io, serializers: serializers, mode: mode, display_adapters: display_adapters)
|
|
5
|
+
@model = PrD::ReportModel.new
|
|
6
|
+
@subject_display_strategy = subject_display_strategy
|
|
7
|
+
@eager_subject_display_strategy = eager_subject_display_strategy
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
attr_reader :model
|
|
11
|
+
|
|
12
|
+
def title(message)
|
|
13
|
+
record(:title, snapshot(message))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def context(message)
|
|
17
|
+
record(:context, snapshot(message))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def success_result(message)
|
|
21
|
+
record(:success_result, snapshot(message))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def failure_result(message)
|
|
25
|
+
record(:failure_result, snapshot(message))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def it(description = nil, &block)
|
|
29
|
+
@current_test_title = description.to_s
|
|
30
|
+
record(:it, snapshot(description))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def end_it(description = nil, &block)
|
|
34
|
+
record(:end_it, snapshot(description))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def justification(justification)
|
|
38
|
+
record(:justification, snapshot(justification))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def let(name_or_value, value = MISSING_VALUE)
|
|
42
|
+
if value.equal?(MISSING_VALUE)
|
|
43
|
+
record(:let, snapshot(name_or_value))
|
|
44
|
+
else
|
|
45
|
+
record(:let, snapshot(name_or_value), snapshot(value))
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def subject(subject)
|
|
50
|
+
record(:subject, snapshot(subject))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def subject_display_strategy
|
|
54
|
+
@subject_display_strategy
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def eager_subject_display_strategy
|
|
58
|
+
@eager_subject_display_strategy
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def pending(description = nil)
|
|
62
|
+
record(:pending, snapshot(description))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def expect(expectation, label: nil)
|
|
66
|
+
record(:expect, snapshot(expectation), label: snapshot(label))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to
|
|
70
|
+
record(:to)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def not_to
|
|
74
|
+
record(:not_to)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def matcher(matcher, sources: nil)
|
|
78
|
+
record(:matcher, snapshot_matcher(matcher), sources: snapshot(sources))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def result(passed_count, failed_count)
|
|
82
|
+
@model.summary = { passed: passed_count, failed: failed_count }
|
|
83
|
+
record(:result, snapshot(passed_count), snapshot(failed_count))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def increment_level
|
|
87
|
+
super
|
|
88
|
+
record(:increment_level)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def decrement_level
|
|
92
|
+
super
|
|
93
|
+
record(:decrement_level)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def flush
|
|
97
|
+
# Rendering happens later from the canonical model.
|
|
98
|
+
@io.flush if @io.respond_to?(:flush)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def record(name, *args, **kwargs)
|
|
104
|
+
@model.add_event(name:, args:, kwargs:)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def snapshot(value)
|
|
108
|
+
@model.snapshot(value)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def snapshot_matcher(matcher)
|
|
112
|
+
clone =
|
|
113
|
+
begin
|
|
114
|
+
matcher.dup
|
|
115
|
+
rescue StandardError
|
|
116
|
+
matcher
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
if clone.instance_variable_defined?(:@expected)
|
|
120
|
+
clone.instance_variable_set(:@expected, snapshot(clone.instance_variable_get(:@expected)))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
if clone.respond_to?(:expected_label) && clone.respond_to?(:expected_label=)
|
|
124
|
+
clone.expected_label = snapshot(clone.expected_label)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
clone
|
|
128
|
+
rescue StandardError
|
|
129
|
+
matcher
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
require 'stringio'
|
|
2
|
+
require 'tempfile'
|
|
3
|
+
|
|
4
|
+
module PrD
|
|
5
|
+
class ReportModel
|
|
6
|
+
FerrumNodeSnapshot = Struct.new(:payload, :summary, keyword_init: true)
|
|
7
|
+
|
|
8
|
+
attr_reader :events
|
|
9
|
+
attr_accessor :summary
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@events = []
|
|
13
|
+
@summary = { passed: 0, failed: 0 }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def add_event(name:, args:, kwargs:)
|
|
17
|
+
@events << { name:, args:, kwargs: }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def snapshot(value)
|
|
21
|
+
return value if value.nil?
|
|
22
|
+
return value if immutable_scalar?(value)
|
|
23
|
+
|
|
24
|
+
if code_object?(value)
|
|
25
|
+
return PrD::Code.new(source: value.source.to_s.dup, language: value.language.to_s.dup)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if ferrum_node?(value)
|
|
29
|
+
return FerrumNodeSnapshot.new(
|
|
30
|
+
payload: ferrum_node_payload_snapshot(value),
|
|
31
|
+
summary: ferrum_node_summary_snapshot(value)
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if file_like?(value)
|
|
36
|
+
return snapshot_file(value)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if pdf_reader?(value)
|
|
40
|
+
return snapshot_pdf_reader(value)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if value.is_a?(Array)
|
|
44
|
+
return value.map { |entry| snapshot(entry) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if value.is_a?(Hash)
|
|
48
|
+
return value.each_with_object({}) { |(key, entry), acc| acc[snapshot(key)] = snapshot(entry) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
begin
|
|
52
|
+
value.dup
|
|
53
|
+
rescue StandardError
|
|
54
|
+
value
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def immutable_scalar?(value)
|
|
61
|
+
value.is_a?(Numeric) || value.is_a?(Symbol) || value.equal?(true) || value.equal?(false)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def code_object?(value)
|
|
65
|
+
defined?(PrD::Code) && value.is_a?(PrD::Code)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def ferrum_node?(value)
|
|
69
|
+
value.respond_to?(:class) && value.class.respond_to?(:name) && value.class.name == 'Ferrum::Node'
|
|
70
|
+
rescue StandardError
|
|
71
|
+
false
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def ferrum_node_payload_snapshot(node)
|
|
75
|
+
return nil unless node.respond_to?(:evaluate)
|
|
76
|
+
|
|
77
|
+
raw_payload = node.evaluate(<<~JS)
|
|
78
|
+
(() => {
|
|
79
|
+
const element = this;
|
|
80
|
+
if (!element) return null;
|
|
81
|
+
|
|
82
|
+
const rawClassName = element.className;
|
|
83
|
+
const className =
|
|
84
|
+
typeof rawClassName === "string" ? rawClassName :
|
|
85
|
+
(rawClassName && typeof rawClassName.baseVal === "string" ? rawClassName.baseVal : "");
|
|
86
|
+
|
|
87
|
+
const classes = className
|
|
88
|
+
.split(/\\s+/)
|
|
89
|
+
.map((token) => token.trim())
|
|
90
|
+
.filter((token) => token.length > 0);
|
|
91
|
+
|
|
92
|
+
const textValue = (element.innerText || element.textContent || "").replace(/\\s+/g, " ").trim();
|
|
93
|
+
const htmlValue = element.outerHTML || "";
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
tag: element.tagName ? element.tagName.toLowerCase() : null,
|
|
97
|
+
id: element.id || null,
|
|
98
|
+
classes,
|
|
99
|
+
text: textValue.length > 160 ? `${textValue.slice(0, 157)}...` : textValue,
|
|
100
|
+
html: htmlValue.length > 220 ? `${htmlValue.slice(0, 217)}...` : htmlValue
|
|
101
|
+
};
|
|
102
|
+
})()
|
|
103
|
+
JS
|
|
104
|
+
return nil unless raw_payload.is_a?(Hash)
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
tag: raw_payload['tag'] || raw_payload[:tag],
|
|
108
|
+
id: raw_payload['id'] || raw_payload[:id],
|
|
109
|
+
classes: raw_payload['classes'] || raw_payload[:classes],
|
|
110
|
+
text: raw_payload['text'] || raw_payload[:text],
|
|
111
|
+
html: raw_payload['html'] || raw_payload[:html]
|
|
112
|
+
}
|
|
113
|
+
rescue StandardError
|
|
114
|
+
{
|
|
115
|
+
tag: safe_node_call(node, :tag_name),
|
|
116
|
+
text: safe_node_call(node, :text),
|
|
117
|
+
description: safe_node_call(node, :description)
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def ferrum_node_summary_snapshot(node)
|
|
122
|
+
return nil unless node.respond_to?(:evaluate)
|
|
123
|
+
|
|
124
|
+
ferrum_node_payload_snapshot(node)
|
|
125
|
+
rescue StandardError
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def safe_node_call(node, method_name)
|
|
130
|
+
return nil unless node.respond_to?(method_name)
|
|
131
|
+
|
|
132
|
+
node.public_send(method_name)
|
|
133
|
+
rescue StandardError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def file_like?(value)
|
|
138
|
+
value.is_a?(File)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def snapshot_file(file)
|
|
142
|
+
file.rewind if file.respond_to?(:rewind)
|
|
143
|
+
bytes = file.read
|
|
144
|
+
file.rewind if file.respond_to?(:rewind)
|
|
145
|
+
ext = File.extname(file.path.to_s)
|
|
146
|
+
temp = Tempfile.new(['prd_snapshot_', ext])
|
|
147
|
+
temp.binmode
|
|
148
|
+
temp.write(bytes || ''.b)
|
|
149
|
+
temp.flush
|
|
150
|
+
temp.rewind
|
|
151
|
+
temp
|
|
152
|
+
rescue StandardError
|
|
153
|
+
file
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def pdf_reader?(value)
|
|
157
|
+
defined?(PDF::Reader) && value.is_a?(PDF::Reader)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def snapshot_pdf_reader(reader)
|
|
161
|
+
return reader unless defined?(PDF::Reader)
|
|
162
|
+
|
|
163
|
+
objects = reader.instance_variable_get(:@objects)
|
|
164
|
+
io = objects&.instance_variable_get(:@io)
|
|
165
|
+
bytes =
|
|
166
|
+
if io.respond_to?(:string)
|
|
167
|
+
io.string
|
|
168
|
+
elsif io.respond_to?(:read)
|
|
169
|
+
current_pos = io.pos if io.respond_to?(:pos)
|
|
170
|
+
content = io.read
|
|
171
|
+
io.seek(current_pos) if io.respond_to?(:seek) && !current_pos.nil?
|
|
172
|
+
content
|
|
173
|
+
end
|
|
174
|
+
return reader if bytes.nil?
|
|
175
|
+
|
|
176
|
+
PDF::Reader.new(StringIO.new(bytes))
|
|
177
|
+
rescue StandardError
|
|
178
|
+
reader
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module PrD
|
|
2
|
+
module ReportRenderer
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def render(model, formatter)
|
|
6
|
+
model.events.each do |event|
|
|
7
|
+
name = event.fetch(:name)
|
|
8
|
+
args = event.fetch(:args)
|
|
9
|
+
kwargs = event.fetch(:kwargs)
|
|
10
|
+
if kwargs.empty?
|
|
11
|
+
formatter.public_send(name, *args)
|
|
12
|
+
else
|
|
13
|
+
formatter.public_send(name, *args, **kwargs)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
formatter.flush
|
|
17
|
+
formatter
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/pr_d/version.rb
CHANGED
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.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Laporte Mathieu
|
|
@@ -132,6 +132,9 @@ files:
|
|
|
132
132
|
- lib/pr_d/matchers/matcher.rb
|
|
133
133
|
- lib/pr_d/mcp/run_specs_tool.rb
|
|
134
134
|
- lib/pr_d/mcp/server.rb
|
|
135
|
+
- lib/pr_d/report_collector.rb
|
|
136
|
+
- lib/pr_d/report_model.rb
|
|
137
|
+
- lib/pr_d/report_renderer.rb
|
|
135
138
|
- lib/pr_d/version.rb
|
|
136
139
|
- lib/probatio_diabolica.rb
|
|
137
140
|
- probatio_diabolica.gemspec
|