hegeltest 0.3.1 → 0.4.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/CHANGELOG.md +262 -0
- data/README.ja.md +408 -0
- data/README.md +209 -103
- data/Rakefile +6 -3
- data/docs/README.md +11 -0
- data/docs/adr/0025-copy-each-string-input-into-a-hegel-string-t.md +61 -0
- data/docs/adr/0026-select-the-engine-release-repository-by-version.md +47 -0
- data/docs/adr/0027-report-each-runs-test-location-from-the-callers-frame.md +63 -0
- data/docs/adr/0028-expose-settings-profiles-as-a-keyword-and-module-functions.md +91 -0
- data/docs/adr/0029-keep-notes-local-and-defer-run-error-kinds.md +72 -0
- data/docs/adr/0030-represent-a-duration-as-integer-nanoseconds.md +48 -0
- data/docs/adr/0031-take-the-alphabet-options-from-hegel-rust.md +52 -0
- data/docs/adr/0032-suppress-span-closes-while-an-abandoned-recursion-attempt-unwinds.md +147 -0
- data/docs/adr/0033-cache-compiled-grammars-for-the-length-of-a-run.md +80 -0
- data/docs/adr/0034-repeat-a-test-body-with-a-ruby-side-indent.md +120 -0
- data/docs/adr/0035-defer-concurrent-state-machines-and-the-engine-printer.md +136 -0
- data/docs/architecture.md +33 -11
- data/lib/hegel/errors.rb +24 -0
- data/lib/hegel/generator.rb +52 -25
- data/lib/hegel/generators.rb +715 -45
- data/lib/hegel/grammar_cache.rb +36 -0
- data/lib/hegel/lib_hegel/real.rb +455 -120
- data/lib/hegel/lib_hegel.rb +33 -8
- data/lib/hegel/libhegel_version.rb +1 -1
- data/lib/hegel/locate.rb +10 -10
- data/lib/hegel/report.rb +13 -10
- data/lib/hegel/runner.rb +61 -27
- data/lib/hegel/settings.rb +92 -16
- data/lib/hegel/state_machine.rb +17 -4
- data/lib/hegel/stateful/pool.rb +52 -0
- data/lib/hegel/stateful.rb +2 -2
- data/lib/hegel/syntax/methods.rb +99 -15
- data/lib/hegel/test_case.rb +256 -35
- data/lib/hegel/test_location.rb +49 -0
- data/lib/hegel/version.rb +1 -1
- data/lib/hegel.rb +21 -5
- data/lib/tasks/libhegel.rake +17 -5
- data/lib/tasks/platform_gems.rake +3 -3
- data/sig/hegel.rbs +296 -43
- data/skills/hegel-ruby/references/ruby/reference.md +465 -43
- metadata +15 -1
data/lib/hegel/stateful/pool.rb
CHANGED
|
@@ -103,6 +103,58 @@ module Hegel
|
|
|
103
103
|
@values.delete(variable_id)
|
|
104
104
|
value
|
|
105
105
|
end
|
|
106
|
+
|
|
107
|
+
# Consumes up to Filtered::MAX_ATTEMPTS ids and keeps only the one
|
|
108
|
+
# whose value +predicate+ accepts. hegel_pool_generate removes an id
|
|
109
|
+
# for good unless hegel_pool_restore puts it back, so a filter that
|
|
110
|
+
# drew through #do_draw would lose every value it rejected. Rejected
|
|
111
|
+
# ids stay consumed while the attempts run, so no attempt draws one
|
|
112
|
+
# again, and go back together once they are over. hegel-rust's
|
|
113
|
+
# ValuesConsumed (frontends/rust/src/stateful.rs) does the same.
|
|
114
|
+
def do_draw_filtered(tc, predicate)
|
|
115
|
+
attempted = []
|
|
116
|
+
accepted = nil
|
|
117
|
+
restore = true
|
|
118
|
+
begin
|
|
119
|
+
Filtered::MAX_ATTEMPTS.times do
|
|
120
|
+
# Once every value left was rejected, the engine has nothing
|
|
121
|
+
# more to draw.
|
|
122
|
+
break if @values.size == attempted.size
|
|
123
|
+
|
|
124
|
+
accepted = filtered_attempt(tc, predicate, attempted)
|
|
125
|
+
break if accepted
|
|
126
|
+
end
|
|
127
|
+
rescue *Hegel::FATAL_EXCEPTIONS
|
|
128
|
+
# The process is ending, and answering a NoMemoryError with
|
|
129
|
+
# native calls is the wrong move (see Hegel::Stateful.apply_rule).
|
|
130
|
+
restore = false
|
|
131
|
+
raise
|
|
132
|
+
ensure
|
|
133
|
+
(attempted - [accepted]).each { |variable_id| tc.pool_restore(@pool, variable_id) } if restore
|
|
134
|
+
end
|
|
135
|
+
raise Hegel::AssumeFailed unless accepted
|
|
136
|
+
|
|
137
|
+
@values.delete(accepted)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
# One attempt, in a span of its own that closes as discarded unless
|
|
143
|
+
# the predicate accepts. Records the consumed id in +attempted+ before
|
|
144
|
+
# the predicate runs, so a predicate that raises still has its id put
|
|
145
|
+
# back. Returns the id when accepted, nil otherwise. +accepted+ stays
|
|
146
|
+
# nil, which discards the span, when a draw or the predicate raises.
|
|
147
|
+
def filtered_attempt(tc, predicate, attempted)
|
|
148
|
+
tc.start_span(Generator::FILTER_ATTEMPT_LABEL)
|
|
149
|
+
begin
|
|
150
|
+
variable_id = tc.pool_generate(@pool, true)
|
|
151
|
+
attempted << variable_id
|
|
152
|
+
accepted = predicate.call(@values.fetch(variable_id))
|
|
153
|
+
variable_id if accepted
|
|
154
|
+
ensure
|
|
155
|
+
tc.stop_span(discard: !accepted)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
106
158
|
end
|
|
107
159
|
end
|
|
108
160
|
end
|
data/lib/hegel/stateful.rb
CHANGED
|
@@ -43,7 +43,7 @@ module Hegel
|
|
|
43
43
|
|
|
44
44
|
invariants = machine.class.invariant_definitions
|
|
45
45
|
state_machine = tc.new_state_machine(
|
|
46
|
-
rules.keys, invariants.keys, invariants.values.map(&:always_run), step_count
|
|
46
|
+
rules.keys, rules.values.map(&:weight), invariants.keys, invariants.values.map(&:always_run), step_count
|
|
47
47
|
)
|
|
48
48
|
begin
|
|
49
49
|
tc.note { "Initial invariant check." }
|
|
@@ -81,7 +81,7 @@ module Hegel
|
|
|
81
81
|
name = rule_names[rule_index]
|
|
82
82
|
steps_attempted += 1
|
|
83
83
|
tc.note { "Step #{steps_attempted}: #{name}" }
|
|
84
|
-
rejected = true unless apply_rule(machine, rules.fetch(name), state_machine, tc)
|
|
84
|
+
rejected = true unless apply_rule(machine, rules.fetch(name).block, state_machine, tc)
|
|
85
85
|
end
|
|
86
86
|
tc.stop_span(discard: rejected)
|
|
87
87
|
run_sampled_invariants(machine, invariants, state_machine, tc)
|
data/lib/hegel/syntax/methods.rb
CHANGED
|
@@ -27,26 +27,34 @@ module Hegel
|
|
|
27
27
|
# A double in [min_value, max_value]. allow_nan and allow_infinity
|
|
28
28
|
# default to what the bounds imply: NaN when the caller passed
|
|
29
29
|
# neither bound, an infinity when the caller left either side open.
|
|
30
|
+
# allow_subnormal defaults to true whenever the bounds admit a
|
|
31
|
+
# subnormal.
|
|
30
32
|
def floats(min_value: nil, max_value: nil, allow_nan: nil, allow_infinity: nil, exclude_min: false,
|
|
31
|
-
exclude_max: false)
|
|
33
|
+
exclude_max: false, allow_subnormal: nil)
|
|
32
34
|
Generators::FloatGenerator.new(
|
|
33
35
|
min_value: min_value, max_value: max_value, allow_nan: allow_nan, allow_infinity: allow_infinity,
|
|
34
|
-
exclude_min: exclude_min, exclude_max: exclude_max
|
|
36
|
+
exclude_min: exclude_min, exclude_max: exclude_max, allow_subnormal: allow_subnormal
|
|
35
37
|
)
|
|
36
38
|
end
|
|
37
39
|
|
|
38
|
-
# A Unicode string of [min_size, max_size] characters
|
|
39
|
-
|
|
40
|
+
# A Unicode string of [min_size, max_size] characters; max_size
|
|
41
|
+
# defaults to 100, or min_size + 100 when min_size exceeds 100.
|
|
42
|
+
# +alphabet+ is a String of the permitted characters; it cannot be
|
|
43
|
+
# combined with the options after it.
|
|
44
|
+
def text(min_size: 0, max_size: nil, alphabet: nil, codec: nil, min_codepoint: nil, max_codepoint: nil,
|
|
45
|
+
categories: nil, exclude_categories: nil, include_characters: nil, exclude_characters: nil)
|
|
40
46
|
Generators::TextGenerator.new(
|
|
41
|
-
min_size: min_size, max_size: max_size, codec: codec,
|
|
42
|
-
min_codepoint: min_codepoint, max_codepoint: max_codepoint
|
|
47
|
+
min_size: min_size, max_size: max_size, alphabet: alphabet, codec: codec,
|
|
48
|
+
min_codepoint: min_codepoint, max_codepoint: max_codepoint, categories: categories,
|
|
49
|
+
exclude_categories: exclude_categories, include_characters: include_characters,
|
|
50
|
+
exclude_characters: exclude_characters
|
|
43
51
|
)
|
|
44
52
|
end
|
|
45
53
|
|
|
46
54
|
# An Array of values from +elements+, with [min_size, max_size]
|
|
47
|
-
# entries.
|
|
48
|
-
def arrays(elements, min_size: 0, max_size: nil)
|
|
49
|
-
Generators::ArrayGenerator.new(elements, min_size: min_size, max_size: max_size)
|
|
55
|
+
# entries. With unique: true, no two entries are equal.
|
|
56
|
+
def arrays(elements, min_size: 0, max_size: nil, unique: false)
|
|
57
|
+
Generators::ArrayGenerator.new(elements, min_size: min_size, max_size: max_size, unique: unique)
|
|
50
58
|
end
|
|
51
59
|
|
|
52
60
|
# Always +value+, drawing nothing.
|
|
@@ -59,6 +67,29 @@ module Hegel
|
|
|
59
67
|
Generators::SampledFromGenerator.new(collection)
|
|
60
68
|
end
|
|
61
69
|
|
|
70
|
+
# An Array holding every element of +elements+ (anything #sampled_from
|
|
71
|
+
# accepts) in an order the engine picks, shrinking towards the
|
|
72
|
+
# original one.
|
|
73
|
+
def permutations(elements)
|
|
74
|
+
Generators::PermutationsGenerator.new(elements)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# An Array holding [min_size, max_size] of the elements of +elements+,
|
|
78
|
+
# in their original order. Duplicate elements count as distinct.
|
|
79
|
+
def subsequences(elements, min_size: 0, max_size: nil)
|
|
80
|
+
Generators::SubsequencesGenerator.new(elements, min_size: min_size, max_size: max_size)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# An Array of [min_size, max_size] elements of +elements+. With
|
|
84
|
+
# +replacement+ (the default) each is drawn independently and the
|
|
85
|
+
# size is unbounded unless +max_size+ bounds it. Without it, each
|
|
86
|
+
# element is used at most once and the size is capped at the number
|
|
87
|
+
# of elements; hegel-rust names the two modes with_replacement and
|
|
88
|
+
# without_replacement.
|
|
89
|
+
def samples(elements, min_size: 0, max_size: nil, replacement: true)
|
|
90
|
+
Generators::SamplesGenerator.new(elements, min_size: min_size, max_size: max_size, replacement: replacement)
|
|
91
|
+
end
|
|
92
|
+
|
|
62
93
|
# A value drawn from one of +generators+, picked at random.
|
|
63
94
|
def one_of(*generators)
|
|
64
95
|
Generators::OneOfGenerator.new(generators)
|
|
@@ -88,20 +119,41 @@ module Hegel
|
|
|
88
119
|
|
|
89
120
|
# A String of exactly one character, sharing #text's own alphabet
|
|
90
121
|
# options.
|
|
91
|
-
def characters(codec: nil, min_codepoint: nil, max_codepoint: nil
|
|
92
|
-
|
|
122
|
+
def characters(codec: nil, min_codepoint: nil, max_codepoint: nil, categories: nil, exclude_categories: nil,
|
|
123
|
+
include_characters: nil, exclude_characters: nil)
|
|
124
|
+
Generators::CharactersGenerator.new(
|
|
125
|
+
codec: codec, min_codepoint: min_codepoint, max_codepoint: max_codepoint, categories: categories,
|
|
126
|
+
exclude_categories: exclude_categories, include_characters: include_characters,
|
|
127
|
+
exclude_characters: exclude_characters
|
|
128
|
+
)
|
|
93
129
|
end
|
|
94
130
|
|
|
95
|
-
# A byte String of [min_size, max_size] bytes
|
|
131
|
+
# A byte String of [min_size, max_size] bytes; max_size defaults as
|
|
132
|
+
# in #text.
|
|
96
133
|
def binary(min_size: 0, max_size: nil)
|
|
97
134
|
Generators::BinaryGenerator.new(min_size: min_size, max_size: max_size)
|
|
98
135
|
end
|
|
99
136
|
|
|
100
137
|
# A String matching +pattern+ (Python `re` syntax, not Ruby's
|
|
101
138
|
# Regexp syntax; see Generators::FromRegexGenerator). fullmatch
|
|
102
|
-
# requires the whole string to match, not just contain
|
|
103
|
-
|
|
104
|
-
|
|
139
|
+
# (the default) requires the whole string to match, not just contain
|
|
140
|
+
# a match.
|
|
141
|
+
# +alphabet+ is a #characters generator limiting every character of the
|
|
142
|
+
# string.
|
|
143
|
+
def from_regex(pattern, fullmatch: true, alphabet: nil)
|
|
144
|
+
Generators::FromRegexGenerator.new(pattern, fullmatch: fullmatch, alphabet: alphabet)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# A String that +grammar+, written in the syntax of the Lark parsing
|
|
148
|
+
# library, accepts from its +start+ rule. +explicit+ maps terminal names
|
|
149
|
+
# to generators of the values drawn for them, +alphabet+ is a
|
|
150
|
+
# #characters generator limiting what terminal patterns draw, and
|
|
151
|
+
# +import_path+ is the directory %import reads files from.
|
|
152
|
+
def from_lark(grammar, start: nil, explicit: {}, alphabet: nil, max_size: nil, max_depth: nil, import_path: nil)
|
|
153
|
+
Generators::FromLarkGenerator.new(
|
|
154
|
+
grammar, start: start, explicit: explicit, alphabet: alphabet, max_size: max_size, max_depth: max_depth,
|
|
155
|
+
import_path: import_path
|
|
156
|
+
)
|
|
105
157
|
end
|
|
106
158
|
|
|
107
159
|
# An RFC 5321/5322 email address String.
|
|
@@ -152,6 +204,29 @@ module Hegel
|
|
|
152
204
|
Generators::DatetimesGenerator.new(min_value: min_value, max_value: max_value)
|
|
153
205
|
end
|
|
154
206
|
|
|
207
|
+
# A "YYYY-MM-DD" String, years 1 through 9999. Takes no options.
|
|
208
|
+
def date_strings
|
|
209
|
+
Generators::DateStringsGenerator.new
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# An "HH:MM:SS" String, with ".fffffffff" appended when the
|
|
213
|
+
# nanosecond is not zero. Takes no options.
|
|
214
|
+
def time_strings
|
|
215
|
+
Generators::TimeStringsGenerator.new
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# A "YYYY-MM-DDTHH:MM:SS[.fffffffff]" String, years 1 through 9999.
|
|
219
|
+
# Takes no options.
|
|
220
|
+
def datetime_strings
|
|
221
|
+
Generators::DatetimeStringsGenerator.new
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# A duration as Integer nanoseconds in [min_value, max_value], up to
|
|
225
|
+
# 2**64 - 1 (see Generators::DurationsGenerator and docs/adr/0030).
|
|
226
|
+
def durations(min_value: 0, max_value: (2**64) - 1)
|
|
227
|
+
Generators::DurationsGenerator.new(min_value: min_value, max_value: max_value)
|
|
228
|
+
end
|
|
229
|
+
|
|
155
230
|
# A value built from imperative code: +block+ receives a draw
|
|
156
231
|
# surface and may call #draw on it any number of times to assemble
|
|
157
232
|
# one value (see Generators::CompositeGenerator).
|
|
@@ -165,6 +240,15 @@ module Hegel
|
|
|
165
240
|
def deferred
|
|
166
241
|
Generators::DeferredGenerator.new
|
|
167
242
|
end
|
|
243
|
+
|
|
244
|
+
# A recursively defined value, such as a tree: either a value from
|
|
245
|
+
# +leaf+, or what the block's generator builds from sub-values of the
|
|
246
|
+
# same kind (see Generators::RecursiveGenerator). The block receives
|
|
247
|
+
# the generator of those sub-values and is called afresh for each
|
|
248
|
+
# branch.
|
|
249
|
+
def recursive(leaf, max_depth: 32, max_leaves: 100, &branch)
|
|
250
|
+
Generators::RecursiveGenerator.new(leaf, max_depth: max_depth, max_leaves: max_leaves, &branch)
|
|
251
|
+
end
|
|
168
252
|
end
|
|
169
253
|
end
|
|
170
254
|
|