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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +262 -0
  3. data/README.ja.md +408 -0
  4. data/README.md +209 -103
  5. data/Rakefile +6 -3
  6. data/docs/README.md +11 -0
  7. data/docs/adr/0025-copy-each-string-input-into-a-hegel-string-t.md +61 -0
  8. data/docs/adr/0026-select-the-engine-release-repository-by-version.md +47 -0
  9. data/docs/adr/0027-report-each-runs-test-location-from-the-callers-frame.md +63 -0
  10. data/docs/adr/0028-expose-settings-profiles-as-a-keyword-and-module-functions.md +91 -0
  11. data/docs/adr/0029-keep-notes-local-and-defer-run-error-kinds.md +72 -0
  12. data/docs/adr/0030-represent-a-duration-as-integer-nanoseconds.md +48 -0
  13. data/docs/adr/0031-take-the-alphabet-options-from-hegel-rust.md +52 -0
  14. data/docs/adr/0032-suppress-span-closes-while-an-abandoned-recursion-attempt-unwinds.md +147 -0
  15. data/docs/adr/0033-cache-compiled-grammars-for-the-length-of-a-run.md +80 -0
  16. data/docs/adr/0034-repeat-a-test-body-with-a-ruby-side-indent.md +120 -0
  17. data/docs/adr/0035-defer-concurrent-state-machines-and-the-engine-printer.md +136 -0
  18. data/docs/architecture.md +33 -11
  19. data/lib/hegel/errors.rb +24 -0
  20. data/lib/hegel/generator.rb +52 -25
  21. data/lib/hegel/generators.rb +715 -45
  22. data/lib/hegel/grammar_cache.rb +36 -0
  23. data/lib/hegel/lib_hegel/real.rb +455 -120
  24. data/lib/hegel/lib_hegel.rb +33 -8
  25. data/lib/hegel/libhegel_version.rb +1 -1
  26. data/lib/hegel/locate.rb +10 -10
  27. data/lib/hegel/report.rb +13 -10
  28. data/lib/hegel/runner.rb +61 -27
  29. data/lib/hegel/settings.rb +92 -16
  30. data/lib/hegel/state_machine.rb +17 -4
  31. data/lib/hegel/stateful/pool.rb +52 -0
  32. data/lib/hegel/stateful.rb +2 -2
  33. data/lib/hegel/syntax/methods.rb +99 -15
  34. data/lib/hegel/test_case.rb +256 -35
  35. data/lib/hegel/test_location.rb +49 -0
  36. data/lib/hegel/version.rb +1 -1
  37. data/lib/hegel.rb +21 -5
  38. data/lib/tasks/libhegel.rake +17 -5
  39. data/lib/tasks/platform_gems.rake +3 -3
  40. data/sig/hegel.rbs +296 -43
  41. data/skills/hegel-ruby/references/ruby/reference.md +465 -43
  42. metadata +15 -1
@@ -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
@@ -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)
@@ -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
- def text(min_size: 0, max_size: nil, codec: nil, min_codepoint: nil, max_codepoint: nil)
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
- Generators::CharactersGenerator.new(codec: codec, min_codepoint: min_codepoint, max_codepoint: max_codepoint)
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 a match.
103
- def from_regex(pattern, fullmatch: false)
104
- Generators::FromRegexGenerator.new(pattern, fullmatch: fullmatch)
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