probatio_diabolica 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +67 -15
- data/bin/prd +121 -40
- data/lib/pr_d/formatters/html_formatter.rb +207 -111
- data/lib/pr_d/formatters/json_formatter.rb +28 -1
- data/lib/pr_d/formatters/pdf_formatter.rb +90 -6
- data/lib/pr_d/version.rb +1 -1
- data/lib/pr_d.rb +6 -0
- data/probatio_diabolica.gemspec +3 -3
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 368faf074dc354becfef975422fc3199a1e83293a30bad935ef17696a9b879c9
|
|
4
|
+
data.tar.gz: 66b84421f306926f425cdc6d9e5cc4a1ff3b8ed1cd4f6df13e86bd7313796c5d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f9cce79af416cc2b0e216820dd3f07f2d0ba46552db2e2598d9ab2ab83f1492757044717a383256db7b6d14168a3da4d11a0cf89949f6ec1eebe4bf55f366f2c
|
|
7
|
+
data.tar.gz: 03e7a8a23b02b473e8bac35c4b4021e729b4e435408bdc69a6143162eebdad8172ec5bb07b83d4471b87464c5f42b1346a112d0e1d7f3af061f916838ea13ea8
|
data/README.md
CHANGED
|
@@ -56,6 +56,28 @@ gem build probatio_diabolica.gemspec
|
|
|
56
56
|
gem install ./probatio_diabolica-*.gem
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
## Release workflow
|
|
60
|
+
|
|
61
|
+
Use the release helper to bump version, refresh `Gemfile.lock`, create commit/tag, and push:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# explicit version
|
|
65
|
+
bin/release --version 0.2.0
|
|
66
|
+
|
|
67
|
+
# or env-style
|
|
68
|
+
VERSION=0.2.0 bin/release
|
|
69
|
+
|
|
70
|
+
# semantic bump from current version
|
|
71
|
+
bin/release --bump patch
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Useful options:
|
|
75
|
+
|
|
76
|
+
- `--dry-run` preview all actions without modifying files/git
|
|
77
|
+
- `--no-push` create commit/tag locally only
|
|
78
|
+
- `--skip-tests` skip `bundle exec ruby bin/prd spec --mode synthetic`
|
|
79
|
+
- `--allow-dirty` bypass clean-working-tree guard
|
|
80
|
+
|
|
59
81
|
After installation, you can run:
|
|
60
82
|
|
|
61
83
|
```bash
|
|
@@ -85,39 +107,63 @@ Without valid configuration, tests using `satisfy(...)` will fail.
|
|
|
85
107
|
|
|
86
108
|
## Running tests
|
|
87
109
|
|
|
88
|
-
|
|
110
|
+
CLI command:
|
|
89
111
|
|
|
90
112
|
```bash
|
|
91
113
|
prd <file_or_directory> [options]
|
|
92
114
|
```
|
|
93
115
|
|
|
94
|
-
From source checkout (without gem install),
|
|
116
|
+
From source checkout (without gem install), this is always valid:
|
|
95
117
|
|
|
96
118
|
```bash
|
|
97
|
-
bundle exec prd <file_or_directory> [options]
|
|
119
|
+
bundle exec ruby bin/prd <file_or_directory> [options]
|
|
98
120
|
```
|
|
99
121
|
|
|
100
|
-
|
|
122
|
+
Options:
|
|
101
123
|
|
|
102
|
-
- `-o, --out DIR` writes output to `DIR/report.qd` (otherwise stdout)
|
|
103
124
|
- `-c, --config FILE` Ruby config file to require
|
|
104
|
-
- `-t, --type TYPE` formatter type
|
|
105
|
-
-
|
|
125
|
+
- `-t, --type TYPE` formatter type(s), default: `simple`
|
|
126
|
+
- supported: `simple`, `html`, `json`, `pdf`
|
|
127
|
+
- can be repeated (`-t html -t json`) or comma-separated (`-t html,json,pdf`)
|
|
128
|
+
- `-o, --out PATH` output base path (directory or file-like base name)
|
|
129
|
+
- `-m, --mode MODE` output mode, default: `verbose`
|
|
130
|
+
- supported: `verbose`, `synthetic`
|
|
131
|
+
|
|
132
|
+
Output rules (`--out`):
|
|
133
|
+
|
|
134
|
+
- No `--out`:
|
|
135
|
+
- one formatter (`simple`, `html`, or `json`): output goes to `stdout`
|
|
136
|
+
- `pdf`: fails (`PDF formatter requires --out`)
|
|
137
|
+
- multiple formatters: fails (`Multiple formatter types require --out`)
|
|
138
|
+
- With `--out PATH`:
|
|
139
|
+
- if `PATH` exists as a directory, or ends with `/`, reports are written as `PATH/report.<ext>`
|
|
140
|
+
- otherwise, `PATH` is treated as a base name and reports are written as `PATH.<ext>`
|
|
141
|
+
- if `PATH` ends with one known extension (`.txt`, `.html`, `.json`, `.pdf`), that extension is stripped before generating outputs
|
|
142
|
+
|
|
143
|
+
Formatter/file extension mapping:
|
|
144
|
+
|
|
145
|
+
- `simple` -> `.txt`
|
|
146
|
+
- `html` -> `.html`
|
|
147
|
+
- `json` -> `.json`
|
|
148
|
+
- `pdf` -> `.pdf`
|
|
106
149
|
|
|
107
150
|
Examples:
|
|
108
151
|
|
|
109
152
|
```bash
|
|
110
153
|
# single file
|
|
111
|
-
bundle exec prd examples/basics_spec.rb
|
|
154
|
+
bundle exec ruby bin/prd examples/basics_spec.rb
|
|
112
155
|
|
|
113
156
|
# all *_spec.rb files in a directory
|
|
114
|
-
bundle exec prd examples
|
|
157
|
+
bundle exec ruby bin/prd examples
|
|
115
158
|
|
|
116
|
-
# HTML report
|
|
117
|
-
bundle exec prd examples/image_spec.rb -t html -o ./tmp
|
|
159
|
+
# HTML report in an existing directory (creates ./tmp/report.html)
|
|
160
|
+
bundle exec ruby bin/prd examples/image_spec.rb -t html -o ./tmp/
|
|
161
|
+
|
|
162
|
+
# multiple reports from one run with shared base name
|
|
163
|
+
bundle exec ruby bin/prd examples/basics_spec.rb -t html,json,pdf -o ./tmp/my_report
|
|
118
164
|
|
|
119
165
|
# compact synthetic output on console
|
|
120
|
-
bundle exec prd examples/basics_spec.rb --mode synthetic
|
|
166
|
+
bundle exec ruby bin/prd examples/basics_spec.rb --mode synthetic
|
|
121
167
|
```
|
|
122
168
|
|
|
123
169
|
## Available DSL
|
|
@@ -210,7 +256,13 @@ The runtime keeps a model stack (`it` can temporarily override the parent `conte
|
|
|
210
256
|
|
|
211
257
|
- `PrD::Formatters::SimpleFormatter` (text console output)
|
|
212
258
|
- `PrD::Formatters::HtmlFormatter` (simple HTML output)
|
|
213
|
-
- `PrD::Formatters::JsonFormatter`
|
|
259
|
+
- `PrD::Formatters::JsonFormatter` (structured JSON output)
|
|
260
|
+
- `PrD::Formatters::PdfFormatter` (PDF report output)
|
|
261
|
+
|
|
262
|
+
In CLI usage:
|
|
263
|
+
|
|
264
|
+
- selecting one formatter writes one output stream/file
|
|
265
|
+
- selecting multiple formatters runs tests once and writes one file per formatter
|
|
214
266
|
|
|
215
267
|
### Subject rendering policy (best effort)
|
|
216
268
|
|
|
@@ -245,5 +297,5 @@ The goal is to preserve readability and report size while surfacing the richest
|
|
|
245
297
|
## Current limitations
|
|
246
298
|
|
|
247
299
|
- Work in progress, API may change.
|
|
248
|
-
-
|
|
249
|
-
-
|
|
300
|
+
- `satisfy(...)` requires a configured LLM provider and network access.
|
|
301
|
+
- PDF and multi-format output require `--out`.
|
data/bin/prd
CHANGED
|
@@ -6,15 +6,93 @@ rescue LoadError
|
|
|
6
6
|
require_relative '../lib/pr_d'
|
|
7
7
|
end
|
|
8
8
|
require 'optparse'
|
|
9
|
+
require 'fileutils'
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
FORMATTERS = {
|
|
12
|
+
'simple' => { klass: PrD::Formatters::SimpleFormatter, extension: '.txt', binary: false },
|
|
13
|
+
'html' => { klass: PrD::Formatters::HtmlFormatter, extension: '.html', binary: false },
|
|
14
|
+
'json' => { klass: PrD::Formatters::JsonFormatter, extension: '.json', binary: false },
|
|
15
|
+
'pdf' => { klass: PrD::Formatters::PdfFormatter, extension: '.pdf', binary: true }
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
SUPPORTED_FORMATTERS = FORMATTERS.keys.freeze
|
|
19
|
+
SUPPORTED_MODES = %w[verbose synthetic].freeze
|
|
20
|
+
DEFAULT_REPORT_BASENAME = 'report'.freeze
|
|
21
|
+
|
|
22
|
+
class CompositeFormatter
|
|
23
|
+
def initialize(formatters)
|
|
24
|
+
@formatters = formatters
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def method_missing(name, *args, &block)
|
|
28
|
+
handled = false
|
|
29
|
+
@formatters.each do |formatter|
|
|
30
|
+
next unless formatter.respond_to?(name)
|
|
31
|
+
|
|
32
|
+
formatter.public_send(name, *args, &block)
|
|
33
|
+
handled = true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
return if handled
|
|
37
|
+
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def respond_to_missing?(name, include_private = false)
|
|
42
|
+
@formatters.any? { |formatter| formatter.respond_to?(name, include_private) } || super
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def parse_formatter_types(raw_values)
|
|
47
|
+
types = Array(raw_values)
|
|
48
|
+
.flat_map { |value| value.to_s.split(',') }
|
|
49
|
+
.map(&:strip)
|
|
50
|
+
.reject(&:empty?)
|
|
51
|
+
types = ['simple'] if types.empty?
|
|
52
|
+
|
|
53
|
+
unknown_types = types - SUPPORTED_FORMATTERS
|
|
54
|
+
unless unknown_types.empty?
|
|
55
|
+
supported = SUPPORTED_FORMATTERS.join(', ')
|
|
56
|
+
raise "Unsupported formatter type: #{unknown_types.first}. Supported: #{supported}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
types.uniq
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def normalize_mode(raw_mode)
|
|
63
|
+
mode = raw_mode || 'verbose'
|
|
64
|
+
supported = SUPPORTED_MODES.join(', ')
|
|
65
|
+
raise "Unsupported mode: #{mode}. Supported: #{supported}" unless SUPPORTED_MODES.include?(mode)
|
|
66
|
+
|
|
67
|
+
mode
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def directory_like_path?(path)
|
|
71
|
+
path.end_with?(File::SEPARATOR) || Dir.exist?(path)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def output_base_path(out_path)
|
|
75
|
+
return nil if out_path.nil?
|
|
76
|
+
|
|
77
|
+
if directory_like_path?(out_path)
|
|
78
|
+
File.join(out_path, DEFAULT_REPORT_BASENAME)
|
|
79
|
+
else
|
|
80
|
+
known_extensions = FORMATTERS.values.map { |config| config[:extension].downcase }
|
|
81
|
+
ext = File.extname(out_path).downcase
|
|
82
|
+
return out_path[...-ext.length] if known_extensions.include?(ext)
|
|
83
|
+
|
|
84
|
+
out_path
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
options = { formatters: [] }
|
|
11
89
|
OptionParser
|
|
12
90
|
.new do |opts|
|
|
13
91
|
opts.banner = 'Usage: prd [options] FILE_OR_DIR'
|
|
14
|
-
opts.on('-o', '--out
|
|
92
|
+
opts.on('-o', '--out PATH', 'Specify output base path (directory or path/name)') { |path| options[:out] = path }
|
|
15
93
|
opts.on('-c', '--config FILE', 'Specify the config file') { |file| options[:config] = file }
|
|
16
|
-
opts.on('-t', '--type TYPE', 'Specify
|
|
17
|
-
options[:
|
|
94
|
+
opts.on('-t', '--type TYPE', 'Specify formatter type(s): simple, html, json, pdf (repeat or comma-separated)') do |type|
|
|
95
|
+
options[:formatters] << type
|
|
18
96
|
end
|
|
19
97
|
opts.on('-m', '--mode MODE', 'Specify output mode (verbose, synthetic)') do |mode|
|
|
20
98
|
options[:mode] = mode
|
|
@@ -36,47 +114,50 @@ tests =
|
|
|
36
114
|
[File.read(input_path)]
|
|
37
115
|
end
|
|
38
116
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
STDOUT
|
|
43
|
-
else
|
|
44
|
-
Dir.mkdir(options[:out]) unless Dir.exist?(options[:out])
|
|
45
|
-
report_filename = case options[:formatter]
|
|
46
|
-
when 'html'
|
|
47
|
-
'report.html'
|
|
48
|
-
when 'pdf'
|
|
49
|
-
'report.pdf'
|
|
50
|
-
else
|
|
51
|
-
'report.qd'
|
|
52
|
-
end
|
|
53
|
-
mode = options[:formatter] == 'pdf' ? 'wb' : 'w'
|
|
54
|
-
File.open(File.join(options[:out], report_filename), mode)
|
|
55
|
-
end
|
|
117
|
+
formatter_types = parse_formatter_types(options[:formatters])
|
|
118
|
+
mode = normalize_mode(options[:mode])
|
|
119
|
+
out_base = output_base_path(options[:out])
|
|
56
120
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
when 'pdf'
|
|
65
|
-
PrD::Formatters::PdfFormatter
|
|
66
|
-
else
|
|
67
|
-
raise "Unsupported formatter type: #{options[:formatter]}. Supported: simple, html, json, pdf"
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
mode = options[:mode] || 'verbose'
|
|
71
|
-
supported_modes = %w[verbose synthetic]
|
|
72
|
-
raise "Unsupported mode: #{mode}. Supported: verbose, synthetic" unless supported_modes.include?(mode)
|
|
121
|
+
if formatter_types.include?('pdf') && out_base.nil?
|
|
122
|
+
raise 'PDF formatter requires --out to write a binary report file.'
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
if formatter_types.length > 1 && out_base.nil?
|
|
126
|
+
raise 'Multiple formatter types require --out to specify where files will be written.'
|
|
127
|
+
end
|
|
73
128
|
|
|
74
129
|
serializers = {
|
|
75
130
|
# Ferrum::Network::Exchange => (->(exchange) { exchange.url }),
|
|
76
131
|
# PDF::Reader => (->(pdf) { pdf.pages.map(&:text).join("\n") })
|
|
77
132
|
}
|
|
78
133
|
|
|
79
|
-
|
|
80
|
-
|
|
134
|
+
destinations =
|
|
135
|
+
if out_base.nil?
|
|
136
|
+
[{ io: STDOUT, formatter: FORMATTERS.fetch(formatter_types.first)[:klass].new(io: STDOUT, serializers:, mode: mode.to_sym) }]
|
|
137
|
+
else
|
|
138
|
+
output_dir = File.dirname(out_base)
|
|
139
|
+
FileUtils.mkdir_p(output_dir)
|
|
140
|
+
formatter_types.map do |type|
|
|
141
|
+
config = FORMATTERS.fetch(type)
|
|
142
|
+
output_path = "#{out_base}#{config[:extension]}"
|
|
143
|
+
io = File.open(output_path, config[:binary] ? 'wb' : 'w')
|
|
144
|
+
{ io:, formatter: config[:klass].new(io:, serializers:, mode: mode.to_sym) }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
formatter =
|
|
149
|
+
if destinations.length == 1
|
|
150
|
+
destinations.first[:formatter]
|
|
151
|
+
else
|
|
152
|
+
CompositeFormatter.new(destinations.map { |destination| destination[:formatter] })
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
runtime_output_dir = out_base.nil? ? nil : File.dirname(out_base)
|
|
156
|
+
runtime = PrD::Runtime.new(formatter:, output_dir: runtime_output_dir, config_file: options[:config])
|
|
81
157
|
runtime.run(tests)
|
|
82
|
-
|
|
158
|
+
exit_status = runtime.success? ? 0 : 1
|
|
159
|
+
destinations.each do |destination|
|
|
160
|
+
io = destination[:io]
|
|
161
|
+
io.close if io != STDOUT
|
|
162
|
+
end
|
|
163
|
+
exit(exit_status)
|
|
@@ -6,14 +6,148 @@ module PrD
|
|
|
6
6
|
class HtmlFormatter < Formatter
|
|
7
7
|
def initialize(io: $stdout, serializers: {}, mode: :verbose)
|
|
8
8
|
super(io: io, serializers: serializers, mode: mode)
|
|
9
|
-
@
|
|
9
|
+
@content = +''
|
|
10
|
+
@index_entries = []
|
|
11
|
+
@anchor_counters = Hash.new(0)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def context(message)
|
|
15
|
+
anchor_id = next_anchor_id('ctx')
|
|
16
|
+
add_index_entry(type: :context, label: message, level: @level, anchor_id:)
|
|
17
|
+
@content << "<h2 class=\"context\" id=\"#{anchor_id}\">#{escape(message)}</h2>"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def success_result(message)
|
|
21
|
+
if synthetic?
|
|
22
|
+
@content << "<div class='status success'>PASS</div>"
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
@content << "<div class='status success'>✓ #{escape(message)}</div>"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def failure_result(message)
|
|
29
|
+
if synthetic?
|
|
30
|
+
@content << "<div class='status failure'>FAIL</div>"
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
@content << "<div class='status failure'>✗ #{escape(message)}</div>"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def it(description = nil, &block)
|
|
37
|
+
@current_test_title = description.to_s
|
|
38
|
+
anchor_id = next_anchor_id('test')
|
|
39
|
+
add_index_entry(type: :test, label: description.to_s, level: @level, anchor_id:)
|
|
40
|
+
@content << "<article class=\"test-card\" id=\"#{anchor_id}\">"
|
|
41
|
+
@content << "<h3 class=\"test-title\">#{escape(description)}</h3>"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def end_it(description = nil, &block)
|
|
45
|
+
@content << '</article>'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def justification(justification)
|
|
49
|
+
return if synthetic?
|
|
50
|
+
@content << "<p class=\"line\"><strong>Justification:</strong> #{escape(justification)}</p>"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def let(value)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def subject(subject)
|
|
57
|
+
return if synthetic?
|
|
58
|
+
@content << '<div class="subject-block">'
|
|
59
|
+
@content << "<p class=\"line\"><strong>Subject:</strong> #{escape(serialize(subject).to_s)}</p>"
|
|
60
|
+
if image_file?(subject)
|
|
61
|
+
@content << "<img src=\"#{image_data_uri(subject.path)}\" alt=\"Subject image\" class=\"subject-image\" />"
|
|
62
|
+
elsif pdf_file?(subject)
|
|
63
|
+
@content << "<embed src=\"#{pdf_data_uri(subject.path)}\" type=\"application/pdf\" class=\"subject-pdf\" />"
|
|
64
|
+
elsif pdf_reader?(subject)
|
|
65
|
+
@content << "<embed src=\"#{pdf_reader_data_uri(subject)}\" type=\"application/pdf\" class=\"subject-pdf\" />"
|
|
66
|
+
end
|
|
67
|
+
@content << '</div>'
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def pending(description = nil)
|
|
71
|
+
pending_label = description || 'Pending test'
|
|
72
|
+
anchor_id = next_anchor_id('pending')
|
|
73
|
+
add_index_entry(type: :pending, label: pending_label, level: @level, anchor_id:)
|
|
74
|
+
|
|
75
|
+
@content << "<article class=\"test-card\" id=\"#{anchor_id}\">"
|
|
76
|
+
@content << "<h3 class=\"test-title\">#{escape(pending_label)}</h3>"
|
|
77
|
+
if synthetic?
|
|
78
|
+
@content << "<div class='status pending'>PENDING</div>"
|
|
79
|
+
else
|
|
80
|
+
@content << "<div class='status pending'>⚠ #{escape(pending_label)}</div>"
|
|
81
|
+
@content << '<p class="line muted">This test is pending and has not been executed.</p>'
|
|
82
|
+
end
|
|
83
|
+
@content << '</article>'
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def expect(expectation)
|
|
87
|
+
return if synthetic?
|
|
88
|
+
@content << "<p class=\"line\"><strong>Expect:</strong> #{escape(serialize(expectation).to_s)}</p>"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def to
|
|
92
|
+
return if synthetic?
|
|
93
|
+
@content << '<p class="line"><strong>To:</strong></p>'
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def not_to
|
|
97
|
+
return if synthetic?
|
|
98
|
+
@content << '<p class="line"><strong>Not to:</strong></p>'
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def matcher(matcher, sources: nil)
|
|
102
|
+
return if synthetic?
|
|
103
|
+
case matcher
|
|
104
|
+
when Matchers::EqMatcher
|
|
105
|
+
@content << "<p class=\"line\"><strong>Matcher:</strong> Be equal to #{escape(serialize(matcher.expected).to_s)}</p>"
|
|
106
|
+
when Matchers::BeMatcher
|
|
107
|
+
@content << "<p class=\"line\"><strong>Matcher:</strong> Be the same object as #{escape(serialize(matcher.expected).to_s)}</p>"
|
|
108
|
+
when Matchers::IncludesMatcher
|
|
109
|
+
@content << "<p class=\"line\"><strong>Matcher:</strong> Include #{escape(serialize(matcher.expected).to_s)}</p>"
|
|
110
|
+
when Matchers::HaveMatcher
|
|
111
|
+
@content << "<p class=\"line\"><strong>Matcher:</strong> Have #{escape(serialize(matcher.expected).to_s)}</p>"
|
|
112
|
+
when Matchers::LlmMatcher
|
|
113
|
+
@content << "<p class=\"line\"><strong>Matcher:</strong> Satisfy condition #{escape(serialize(matcher.expected).to_s)}</p>"
|
|
114
|
+
when Matchers::AllMatcher
|
|
115
|
+
if sources
|
|
116
|
+
code_line = matcher.expected.source_location.last.to_i
|
|
117
|
+
code = sources.lines[code_line - 1]
|
|
118
|
+
@content << "<p class=\"line\"><strong>Matcher:</strong> All match condition #{escape(code.strip)}</p>"
|
|
119
|
+
else
|
|
120
|
+
@content << '<p class="line"><strong>Matcher:</strong> All match the given condition</p>'
|
|
121
|
+
end
|
|
122
|
+
else
|
|
123
|
+
@content << "<p class=\"line\"><strong>Matcher:</strong> #{escape(matcher.class.to_s)}</p>"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def result(passed_count, failed_count)
|
|
128
|
+
summary_class = failed_count > 0 ? 'failure' : 'success'
|
|
129
|
+
@content << "<p class='result #{summary_class}'><strong>#{passed_count} passed, #{failed_count} failed</strong></p>"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def flush
|
|
133
|
+
@io << document_opening
|
|
134
|
+
@io << render_index
|
|
135
|
+
@io << @content
|
|
136
|
+
@io << '</main></body></html>'
|
|
137
|
+
super
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def document_opening
|
|
143
|
+
<<~HTML
|
|
10
144
|
<html>
|
|
11
145
|
<head>
|
|
12
146
|
<meta charset="utf-8" />
|
|
13
147
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
14
148
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
15
149
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
16
|
-
<link href="https://fonts.googleapis.com/css2?family=
|
|
150
|
+
<link href="https://fonts.googleapis.com/css2?family=Saira:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
|
|
17
151
|
<style>
|
|
18
152
|
:root {
|
|
19
153
|
--bg: #f3f4f6;
|
|
@@ -48,8 +182,42 @@ module PrD
|
|
|
48
182
|
border-radius: 18px;
|
|
49
183
|
}
|
|
50
184
|
|
|
185
|
+
.report-index {
|
|
186
|
+
background: var(--paper);
|
|
187
|
+
border: 1px solid var(--line);
|
|
188
|
+
border-radius: 14px;
|
|
189
|
+
padding: 0.9rem 1rem;
|
|
190
|
+
margin-bottom: 1rem;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.index-title {
|
|
194
|
+
margin: 0 0 0.6rem;
|
|
195
|
+
font-family: "Saira", Georgia, serif;
|
|
196
|
+
font-size: 1.15rem;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.index-list {
|
|
200
|
+
margin: 0;
|
|
201
|
+
padding: 0;
|
|
202
|
+
list-style: none;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.index-item {
|
|
206
|
+
margin: 0.2rem 0;
|
|
207
|
+
padding-left: calc(var(--index-level, 0) * 1rem);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.index-item a {
|
|
211
|
+
color: var(--accent);
|
|
212
|
+
text-decoration: none;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.index-item a:hover {
|
|
216
|
+
text-decoration: underline;
|
|
217
|
+
}
|
|
218
|
+
|
|
51
219
|
.context {
|
|
52
|
-
font-family: "
|
|
220
|
+
font-family: "Saira", Georgia, serif;
|
|
53
221
|
font-size: clamp(1.4rem, 2.8vw, 2rem);
|
|
54
222
|
margin: 1.3rem 0 1rem;
|
|
55
223
|
padding-bottom: 0.45rem;
|
|
@@ -67,7 +235,7 @@ module PrD
|
|
|
67
235
|
|
|
68
236
|
.test-title {
|
|
69
237
|
margin: 0 0 0.75rem;
|
|
70
|
-
font-family: "
|
|
238
|
+
font-family: "Saira", Georgia, serif;
|
|
71
239
|
font-size: 1.2rem;
|
|
72
240
|
}
|
|
73
241
|
|
|
@@ -153,126 +321,54 @@ module PrD
|
|
|
153
321
|
HTML
|
|
154
322
|
end
|
|
155
323
|
|
|
156
|
-
def
|
|
157
|
-
return if
|
|
158
|
-
@io << "<h2 class=\"context\">#{escape(message)}</h2>"
|
|
159
|
-
end
|
|
324
|
+
def render_index
|
|
325
|
+
return '' if @index_entries.empty?
|
|
160
326
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return
|
|
165
|
-
end
|
|
166
|
-
@io << "<div class='status success'>✓ #{escape(message)}</div>"
|
|
167
|
-
end
|
|
327
|
+
index_items = @index_entries.map do |entry|
|
|
328
|
+
"<li class=\"index-item\" style=\"--index-level: #{entry[:level]};\"><a href=\"##{entry[:anchor_id]}\">#{escape(index_label(entry))}</a></li>"
|
|
329
|
+
end.join
|
|
168
330
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def it(description = nil, &block)
|
|
178
|
-
@current_test_title = description.to_s
|
|
179
|
-
@io << '<article class="test-card">'
|
|
180
|
-
@io << "<h3 class=\"test-title\">#{escape(description)}</h3>"
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
def end_it(description = nil, &block)
|
|
184
|
-
@io << '</article>'
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def justification(justification)
|
|
188
|
-
return if synthetic?
|
|
189
|
-
@io << "<p class=\"line\"><strong>Justification:</strong> #{escape(justification)}</p>"
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def let(value)
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def subject(subject)
|
|
196
|
-
return if synthetic?
|
|
197
|
-
@io << '<div class="subject-block">'
|
|
198
|
-
@io << "<p class=\"line\"><strong>Subject:</strong> #{escape(serialize(subject).to_s)}</p>"
|
|
199
|
-
if image_file?(subject)
|
|
200
|
-
@io << "<img src=\"#{image_data_uri(subject.path)}\" alt=\"Subject image\" class=\"subject-image\" />"
|
|
201
|
-
elsif pdf_file?(subject)
|
|
202
|
-
@io << "<embed src=\"#{pdf_data_uri(subject.path)}\" type=\"application/pdf\" class=\"subject-pdf\" />"
|
|
203
|
-
elsif pdf_reader?(subject)
|
|
204
|
-
@io << "<embed src=\"#{pdf_reader_data_uri(subject)}\" type=\"application/pdf\" class=\"subject-pdf\" />"
|
|
205
|
-
end
|
|
206
|
-
@io << '</div>'
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def pending(description = nil)
|
|
210
|
-
if synthetic?
|
|
211
|
-
@io << '<article class="test-card">'
|
|
212
|
-
@io << "<h3 class=\"test-title\">#{escape(description || 'Pending test')}</h3>"
|
|
213
|
-
@io << "<div class='status pending'>PENDING</div>"
|
|
214
|
-
@io << '</article>'
|
|
215
|
-
return
|
|
216
|
-
end
|
|
217
|
-
@io << "<div class='status pending'>⚠ #{escape(description || 'Pending test')}</div>"
|
|
218
|
-
@io << '<p class="line muted">This test is pending and has not been executed.</p>'
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def expect(expectation)
|
|
222
|
-
return if synthetic?
|
|
223
|
-
@io << "<p class=\"line\"><strong>Expect:</strong> #{escape(serialize(expectation).to_s)}</p>"
|
|
331
|
+
<<~HTML
|
|
332
|
+
<nav class="report-index" aria-label="Report index">
|
|
333
|
+
<h2 class="index-title">Index</h2>
|
|
334
|
+
<ul class="index-list">
|
|
335
|
+
#{index_items}
|
|
336
|
+
</ul>
|
|
337
|
+
</nav>
|
|
338
|
+
HTML
|
|
224
339
|
end
|
|
225
340
|
|
|
226
|
-
def
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
341
|
+
def index_label(entry)
|
|
342
|
+
prefix =
|
|
343
|
+
case entry[:type]
|
|
344
|
+
when :context then 'Context'
|
|
345
|
+
when :pending then 'Pending'
|
|
346
|
+
else 'Test'
|
|
347
|
+
end
|
|
230
348
|
|
|
231
|
-
|
|
232
|
-
return if synthetic?
|
|
233
|
-
@io << '<p class="line"><strong>Not to:</strong></p>'
|
|
349
|
+
"#{prefix}: #{entry[:label]}"
|
|
234
350
|
end
|
|
235
351
|
|
|
236
|
-
def
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
when Matchers::IncludesMatcher
|
|
244
|
-
@io << "<p class=\"line\"><strong>Matcher:</strong> Include #{escape(serialize(matcher.expected).to_s)}</p>"
|
|
245
|
-
when Matchers::HaveMatcher
|
|
246
|
-
@io << "<p class=\"line\"><strong>Matcher:</strong> Have #{escape(serialize(matcher.expected).to_s)}</p>"
|
|
247
|
-
when Matchers::LlmMatcher
|
|
248
|
-
@io << "<p class=\"line\"><strong>Matcher:</strong> Satisfy condition #{escape(serialize(matcher.expected).to_s)}</p>"
|
|
249
|
-
when Matchers::AllMatcher
|
|
250
|
-
if sources
|
|
251
|
-
code_line = matcher.expected.source_location.last.to_i
|
|
252
|
-
code = sources.lines[code_line - 1]
|
|
253
|
-
@io << "<p class=\"line\"><strong>Matcher:</strong> All match condition #{escape(code.strip)}</p>"
|
|
254
|
-
else
|
|
255
|
-
@io << '<p class="line"><strong>Matcher:</strong> All match the given condition</p>'
|
|
256
|
-
end
|
|
257
|
-
else
|
|
258
|
-
@io << "<p class=\"line\"><strong>Matcher:</strong> #{escape(matcher.class.to_s)}</p>"
|
|
259
|
-
end
|
|
352
|
+
def add_index_entry(type:, label:, level:, anchor_id:)
|
|
353
|
+
@index_entries << {
|
|
354
|
+
type:,
|
|
355
|
+
label: normalize_text(label),
|
|
356
|
+
level: [level, 0].max,
|
|
357
|
+
anchor_id:
|
|
358
|
+
}
|
|
260
359
|
end
|
|
261
360
|
|
|
262
|
-
def
|
|
263
|
-
|
|
264
|
-
|
|
361
|
+
def next_anchor_id(prefix)
|
|
362
|
+
@anchor_counters[prefix] += 1
|
|
363
|
+
"#{prefix}-#{@anchor_counters[prefix]}"
|
|
265
364
|
end
|
|
266
365
|
|
|
267
|
-
def
|
|
268
|
-
|
|
269
|
-
super
|
|
366
|
+
def escape(message)
|
|
367
|
+
CGI.escape_html(normalize_text(message))
|
|
270
368
|
end
|
|
271
369
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def escape(message)
|
|
275
|
-
CGI.escape_html(message.to_s)
|
|
370
|
+
def normalize_text(value)
|
|
371
|
+
value.to_s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
|
|
276
372
|
end
|
|
277
373
|
|
|
278
374
|
def image_file?(value)
|
|
@@ -52,6 +52,11 @@ module PrD
|
|
|
52
52
|
add_event(type: 'justification', message: justification)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
def let(value)
|
|
56
|
+
return if synthetic?
|
|
57
|
+
add_event(type: 'let', value: serialize(value))
|
|
58
|
+
end
|
|
59
|
+
|
|
55
60
|
def pending(description = nil)
|
|
56
61
|
if synthetic?
|
|
57
62
|
add_event(type: 'test_result', title: description || 'Pending test', status: 'PENDING')
|
|
@@ -104,7 +109,8 @@ module PrD
|
|
|
104
109
|
private
|
|
105
110
|
|
|
106
111
|
def add_event(type:, **payload)
|
|
107
|
-
|
|
112
|
+
event = payload.merge(type:, level: @level)
|
|
113
|
+
@events << normalize_for_json(event)
|
|
108
114
|
end
|
|
109
115
|
|
|
110
116
|
def serialize(value)
|
|
@@ -189,6 +195,27 @@ module PrD
|
|
|
189
195
|
'application/octet-stream'
|
|
190
196
|
end
|
|
191
197
|
end
|
|
198
|
+
|
|
199
|
+
def normalize_for_json(value)
|
|
200
|
+
case value
|
|
201
|
+
when String
|
|
202
|
+
value.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
|
|
203
|
+
when Array
|
|
204
|
+
value.map { |item| normalize_for_json(item) }
|
|
205
|
+
when Hash
|
|
206
|
+
value.each_with_object({}) do |(key, item), normalized|
|
|
207
|
+
normalized[normalize_hash_key(key)] = normalize_for_json(item)
|
|
208
|
+
end
|
|
209
|
+
else
|
|
210
|
+
value
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def normalize_hash_key(key)
|
|
215
|
+
return key.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') if key.is_a?(String)
|
|
216
|
+
|
|
217
|
+
key
|
|
218
|
+
end
|
|
192
219
|
end
|
|
193
220
|
end
|
|
194
221
|
end
|
|
@@ -18,6 +18,8 @@ module PrD
|
|
|
18
18
|
super(io: io, serializers: serializers, mode: mode)
|
|
19
19
|
@events = []
|
|
20
20
|
@summary = { passed: 0, failed: 0 }
|
|
21
|
+
@index_entries = []
|
|
22
|
+
@anchor_counters = Hash.new(0)
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def title(message)
|
|
@@ -26,8 +28,13 @@ module PrD
|
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def context(message)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
anchor_id = next_anchor_id('ctx')
|
|
32
|
+
add_index_entry(type: :context, label: message, level: @level, anchor_id:)
|
|
33
|
+
if synthetic?
|
|
34
|
+
add_event(:anchor_marker, message: '', level: @level, anchor_id:)
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
add_event(:context, message:, level: @level, anchor_id:)
|
|
31
38
|
end
|
|
32
39
|
|
|
33
40
|
def success_result(message)
|
|
@@ -48,7 +55,9 @@ module PrD
|
|
|
48
55
|
|
|
49
56
|
def it(description = nil, &block)
|
|
50
57
|
@current_test_title = description.to_s
|
|
51
|
-
|
|
58
|
+
anchor_id = next_anchor_id('test')
|
|
59
|
+
add_index_entry(type: :test, label: description.to_s, level: @level, anchor_id:)
|
|
60
|
+
add_event(:it, message: description.to_s, level: @level, anchor_id:)
|
|
52
61
|
end
|
|
53
62
|
|
|
54
63
|
def end_it(description = nil, &block)
|
|
@@ -74,7 +83,10 @@ module PrD
|
|
|
74
83
|
end
|
|
75
84
|
|
|
76
85
|
def pending(description = nil)
|
|
77
|
-
|
|
86
|
+
pending_label = description || 'Pending test'
|
|
87
|
+
anchor_id = next_anchor_id('pending')
|
|
88
|
+
add_index_entry(type: :pending, label: pending_label, level: @level, anchor_id:)
|
|
89
|
+
add_event(:pending, message: pending_label, level: @level, anchor_id:)
|
|
78
90
|
return if synthetic?
|
|
79
91
|
add_event(:detail, message: 'This test is pending and has not been executed.', level: @level + 1)
|
|
80
92
|
end
|
|
@@ -122,6 +134,7 @@ module PrD
|
|
|
122
134
|
def flush
|
|
123
135
|
document = Prawn::Document.new(page_size: 'A4', margin: 42)
|
|
124
136
|
render_header(document)
|
|
137
|
+
render_index(document)
|
|
125
138
|
render_events(document)
|
|
126
139
|
render_summary(document)
|
|
127
140
|
|
|
@@ -132,8 +145,22 @@ module PrD
|
|
|
132
145
|
|
|
133
146
|
private
|
|
134
147
|
|
|
135
|
-
def add_event(type, message:, level:)
|
|
136
|
-
@events << {
|
|
148
|
+
def add_event(type, message:, level:, anchor_id: nil)
|
|
149
|
+
@events << {
|
|
150
|
+
type:,
|
|
151
|
+
message: safe_pdf_text(message.to_s),
|
|
152
|
+
level: [level, 0].max,
|
|
153
|
+
anchor_id:
|
|
154
|
+
}
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def add_index_entry(type:, label:, level:, anchor_id:)
|
|
158
|
+
@index_entries << {
|
|
159
|
+
type:,
|
|
160
|
+
label: safe_pdf_text(label.to_s),
|
|
161
|
+
level: [level, 0].max,
|
|
162
|
+
anchor_id:
|
|
163
|
+
}
|
|
137
164
|
end
|
|
138
165
|
|
|
139
166
|
def safe_pdf_text(text)
|
|
@@ -153,8 +180,40 @@ module PrD
|
|
|
153
180
|
document.move_down 10
|
|
154
181
|
end
|
|
155
182
|
|
|
183
|
+
def render_index(document)
|
|
184
|
+
return if @index_entries.empty?
|
|
185
|
+
|
|
186
|
+
document.fill_color COLORS[:title]
|
|
187
|
+
document.text 'Index', size: 14, style: :bold
|
|
188
|
+
document.move_down 4
|
|
189
|
+
|
|
190
|
+
@index_entries.each do |entry|
|
|
191
|
+
document.indent(entry[:level] * 12) do
|
|
192
|
+
document.formatted_text(
|
|
193
|
+
[
|
|
194
|
+
{
|
|
195
|
+
text: "- #{index_label(entry)}",
|
|
196
|
+
anchor: entry[:anchor_id],
|
|
197
|
+
styles: [:underline],
|
|
198
|
+
color: COLORS[:context]
|
|
199
|
+
}
|
|
200
|
+
],
|
|
201
|
+
size: 10
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
document.move_down 1
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
document.move_down 8
|
|
208
|
+
document.stroke_color COLORS[:border]
|
|
209
|
+
document.stroke_horizontal_rule
|
|
210
|
+
document.stroke_color '000000'
|
|
211
|
+
document.move_down 8
|
|
212
|
+
end
|
|
213
|
+
|
|
156
214
|
def render_events(document)
|
|
157
215
|
@events.each do |event|
|
|
216
|
+
register_anchor(document, event)
|
|
158
217
|
case event[:type]
|
|
159
218
|
when :title, :context
|
|
160
219
|
styled_line(document, event[:message], level: event[:level], size: 14, style: :bold, color: COLORS[:context], spacing: 6)
|
|
@@ -172,6 +231,8 @@ module PrD
|
|
|
172
231
|
styled_line(document, event[:message], level: event[:level], size: 10, color: COLORS[:text])
|
|
173
232
|
when :subject_image
|
|
174
233
|
render_image(document, event[:message], level: event[:level])
|
|
234
|
+
when :anchor_marker
|
|
235
|
+
next
|
|
175
236
|
when :result
|
|
176
237
|
document.move_down 8
|
|
177
238
|
styled_line(document, event[:message], level: event[:level], size: 11, style: :bold, color: COLORS[:title])
|
|
@@ -179,6 +240,13 @@ module PrD
|
|
|
179
240
|
end
|
|
180
241
|
end
|
|
181
242
|
|
|
243
|
+
def register_anchor(document, event)
|
|
244
|
+
anchor_id = event[:anchor_id]
|
|
245
|
+
return if anchor_id.nil?
|
|
246
|
+
|
|
247
|
+
document.add_dest(anchor_id, document.dest_xyz(0, document.cursor))
|
|
248
|
+
end
|
|
249
|
+
|
|
182
250
|
def render_summary(document)
|
|
183
251
|
document.move_down 10
|
|
184
252
|
document.stroke_color COLORS[:border]
|
|
@@ -216,6 +284,22 @@ module PrD
|
|
|
216
284
|
document.move_down 2
|
|
217
285
|
end
|
|
218
286
|
|
|
287
|
+
def index_label(entry)
|
|
288
|
+
prefix =
|
|
289
|
+
case entry[:type]
|
|
290
|
+
when :context then 'Context'
|
|
291
|
+
when :pending then 'Pending'
|
|
292
|
+
else 'Test'
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
"#{prefix}: #{entry[:label]}"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def next_anchor_id(prefix)
|
|
299
|
+
@anchor_counters[prefix] += 1
|
|
300
|
+
"#{prefix}-#{@anchor_counters[prefix]}"
|
|
301
|
+
end
|
|
302
|
+
|
|
219
303
|
def image_file?(value)
|
|
220
304
|
value.is_a?(File) && value.path.match?(/\.(png|jpe?g)\z/i)
|
|
221
305
|
end
|
data/lib/pr_d/version.rb
CHANGED
data/lib/pr_d.rb
CHANGED
|
@@ -69,6 +69,8 @@ module PrD
|
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
attr_reader :passed_count, :failed_count
|
|
73
|
+
|
|
72
74
|
def describe(description, model: nil, &block)
|
|
73
75
|
context(description, model: model, &block)
|
|
74
76
|
@formatter.result(@passed_count, @failed_count)
|
|
@@ -185,6 +187,10 @@ module PrD
|
|
|
185
187
|
@formatter.flush
|
|
186
188
|
end
|
|
187
189
|
|
|
190
|
+
def success?
|
|
191
|
+
@failed_count.zero?
|
|
192
|
+
end
|
|
193
|
+
|
|
188
194
|
private
|
|
189
195
|
|
|
190
196
|
def formatted_time
|
data/probatio_diabolica.gemspec
CHANGED
|
@@ -10,13 +10,13 @@ Gem::Specification.new do |spec|
|
|
|
10
10
|
|
|
11
11
|
spec.summary = "A Ruby DSL testing framework with classic and LLM-powered matchers."
|
|
12
12
|
spec.description = "Probatio Diabolica runs custom *_spec.rb files with a DSL inspired by RSpec and supports text/image/PDF reporting."
|
|
13
|
-
spec.homepage = "https://github.com/
|
|
13
|
+
spec.homepage = "https://github.com/mathieulaporte/probatio_diabolica"
|
|
14
14
|
spec.license = "MIT"
|
|
15
15
|
spec.required_ruby_version = ">= 3.1"
|
|
16
16
|
|
|
17
17
|
spec.metadata = {
|
|
18
|
-
"source_code_uri" => "https://github.com/
|
|
19
|
-
"changelog_uri" => "https://github.com/
|
|
18
|
+
"source_code_uri" => "https://github.com/mathieulaporte/probatio_diabolica",
|
|
19
|
+
"changelog_uri" => "https://github.com/mathieulaporte/probatio_diabolica/releases"
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
spec.files = Dir[
|
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
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Laporte Mathieu
|
|
@@ -111,12 +111,12 @@ files:
|
|
|
111
111
|
- lib/pr_d/version.rb
|
|
112
112
|
- lib/probatio_diabolica.rb
|
|
113
113
|
- probatio_diabolica.gemspec
|
|
114
|
-
homepage: https://github.com/
|
|
114
|
+
homepage: https://github.com/mathieulaporte/probatio_diabolica
|
|
115
115
|
licenses:
|
|
116
116
|
- MIT
|
|
117
117
|
metadata:
|
|
118
|
-
source_code_uri: https://github.com/
|
|
119
|
-
changelog_uri: https://github.com/
|
|
118
|
+
source_code_uri: https://github.com/mathieulaporte/probatio_diabolica
|
|
119
|
+
changelog_uri: https://github.com/mathieulaporte/probatio_diabolica/releases
|
|
120
120
|
rdoc_options: []
|
|
121
121
|
require_paths:
|
|
122
122
|
- lib
|