hegeltest 0.1.1 → 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/CHANGELOG.md +82 -0
- data/README.md +32 -9
- data/Rakefile +81 -0
- data/docs/README.md +9 -0
- data/docs/adr/0015-follow-the-hegeldev-interface-and-take-changes-upstream-first.md +101 -0
- data/docs/adr/0016-run-mutation-testing-with-mutineer.md +87 -0
- data/docs/adr/0017-raise-the-ruby-floor-to-3-4.md +43 -0
- data/docs/adr/0018-gate-mutation-testing-on-a-committed-baseline.md +74 -0
- data/docs/adr/0019-report-failures-from-the-cases-the-engine-stamps.md +70 -0
- data/docs/adr/0020-derive-span-labels-from-generator-names.md +64 -0
- data/docs/adr/0021-run-a-state-machine-in-rounds-with-its-own-step-count.md +59 -0
- data/docs/adr/0022-keep-microsecond-times-over-a-nanosecond-engine.md +45 -0
- data/docs/adr/0023-leave-unset-settings-to-the-engines-profile.md +50 -0
- data/docs/architecture.md +11 -6
- data/lib/hegel/draw_name.rb +3 -1
- data/lib/hegel/generator.rb +50 -9
- data/lib/hegel/generators.rb +134 -102
- data/lib/hegel/lib_hegel/real.rb +193 -94
- data/lib/hegel/lib_hegel.rb +30 -56
- data/lib/hegel/libhegel_version.rb +1 -1
- data/lib/hegel/report.rb +17 -8
- data/lib/hegel/runner.rb +153 -201
- data/lib/hegel/settings.rb +9 -14
- data/lib/hegel/state_machine.rb +15 -8
- data/lib/hegel/stateful/pool.rb +0 -2
- data/lib/hegel/stateful.rb +64 -36
- data/lib/hegel/syntax/methods.rb +3 -2
- data/lib/hegel/test_case.rb +22 -8
- data/lib/hegel/version.rb +1 -1
- data/lib/hegel.rb +11 -17
- data/lib/tasks/libhegel.rake +9 -2
- data/sig/hegel.rbs +58 -60
- data/skills/hegel-ruby/references/ruby/reference.md +96 -52
- metadata +11 -2
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
- [Setup](#setup)
|
|
6
6
|
- [Test Structure](#test-structure): `Hegel.test`, its block, return value, failure behavior
|
|
7
|
-
- [Settings](#settings): `test_cases:` `seed:` `derandomize:` `verbosity:` `database:` `database_key:` `phases:` `suppress_health_check:` `report_multiple_failures:` `
|
|
7
|
+
- [Settings](#settings): `test_cases:` `seed:` `derandomize:` `verbosity:` `database:` `database_key:` `phases:` `suppress_health_check:` `report_multiple_failures:` `output:` `reproduce_failure:`
|
|
8
8
|
- [TestCase Methods](#testcase-methods): `draw`, `draw_integer`, `draw_boolean`, `assume`, `reject`, `note`, `target`
|
|
9
9
|
- [Generator Reference](#generator-reference): `booleans`, `integers`, `floats`, `text`, `arrays`, `just`, `sampled_from`, `one_of`, `optional`, `tuples`, `sets`, `hashes`, `characters`, `binary`, `from_regex`, `emails`, `urls`, `domains`, `ip_addresses`, `uuids`, `dates`, `times`, `datetimes`, `composite`, `deferred`
|
|
10
10
|
- [Stateful Testing](#stateful-testing): `Hegel::StateMachine`, `rule`, `invariant`, `Hegel::Stateful.run`, `Hegel::Stateful::Pool`
|
|
@@ -104,7 +104,7 @@ output.string
|
|
|
104
104
|
`output.string` holds:
|
|
105
105
|
|
|
106
106
|
```
|
|
107
|
-
Falsified after
|
|
107
|
+
Falsified after 12 test cases (0 discarded):
|
|
108
108
|
|
|
109
109
|
xs = [0, 0]
|
|
110
110
|
|
|
@@ -114,7 +114,9 @@ To reproduce this failure, pass the blob below to Hegel.test:
|
|
|
114
114
|
|
|
115
115
|
Hegel names each drawn value (`xs` above) by reading the line the `draw`
|
|
116
116
|
call was written on. Pass the blob back through `reproduce_failure:` to
|
|
117
|
-
replay that
|
|
117
|
+
replay that failure without a full run. The engine replays it until a
|
|
118
|
+
replay fails, within its own budget, and raises `Hegel::Error` when none
|
|
119
|
+
does:
|
|
118
120
|
|
|
119
121
|
```ruby
|
|
120
122
|
Hegel.test(reproduce_failure: "AXicY2VgYGBkZOBiZEBhMAAAAd8AIQ==", verbosity: :quiet) do |tc|
|
|
@@ -123,6 +125,17 @@ Hegel.test(reproduce_failure: "AXicY2VgYGBkZOBiZEBhMAAAAd8AIQ==", verbosity: :qu
|
|
|
123
125
|
end
|
|
124
126
|
```
|
|
125
127
|
|
|
128
|
+
A body that fails only some of the time is reported like any other failure,
|
|
129
|
+
with one more line from the engine that says how reliably it reproduced:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
note: nondeterministic failure, confirmed: failed 6 of 20 replays at confirmation and 1 of 3 at report time
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The blob of such a failure still works with `reproduce_failure:`. A failure
|
|
136
|
+
the engine could not confirm comes with no blob, and its report leaves out
|
|
137
|
+
the reproduction line.
|
|
138
|
+
|
|
126
139
|
## Settings
|
|
127
140
|
|
|
128
141
|
`Hegel.test` takes these keywords, all optional:
|
|
@@ -138,15 +151,19 @@ end
|
|
|
138
151
|
| `phases` | `Array` of `Symbol` or `nil` | `nil` (libhegel's own default: every phase) | Which run phases to enable: `:explicit`, `:reuse`, `:generate`, `:target`, `:shrink` |
|
|
139
152
|
| `suppress_health_check` | `Array` of `Symbol` or `nil` | `nil` (no suppression) | Which health checks to turn off: `:filter_too_much`, `:too_slow`, `:test_cases_too_large`, `:large_initial_test_case` |
|
|
140
153
|
| `report_multiple_failures` | `true`/`false` | `false` | `true` summarizes every distinct failure into one `Hegel::Error` instead of re-raising a single failure's own exception |
|
|
141
|
-
| `stateful_step_count` | `Integer` or `nil` | `nil` (libhegel's own default: 50) | Steps per test case a `Hegel::Stateful.run` call applies, for a stateful test |
|
|
142
154
|
| `output` | `IO` | `$stderr` | Where a failure report is written |
|
|
143
|
-
| `reproduce_failure` | `String` or `nil` | `nil` | Replays the
|
|
155
|
+
| `reproduce_failure` | `String` or `nil` | `nil` | Replays the blob as a run until a replay fails, instead of running a full property |
|
|
144
156
|
|
|
145
157
|
`nil` means the same thing for `test_cases`, `seed`, `derandomize`,
|
|
146
|
-
`verbosity`, `phases`,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
158
|
+
`verbosity`, `phases`, and `suppress_health_check`: do not call the matching
|
|
159
|
+
libhegel setter, and let the engine's settings profile decide. That is
|
|
160
|
+
libhegel's own default, unless a `hegel.toml` or a `HEGEL_*` environment
|
|
161
|
+
variable (such as `HEGEL_TEST_CASES`) sets the value. On a CI server the
|
|
162
|
+
engine picks its `ci` profile, which derandomizes the run, turns the example
|
|
163
|
+
database off, and suppresses the `too_slow` health check. A keyword passed
|
|
164
|
+
explicitly wins over all of these. `database`/`database_key` and
|
|
165
|
+
`report_multiple_failures` each follow their own rule instead, covered
|
|
166
|
+
below.
|
|
150
167
|
|
|
151
168
|
`database_key:` is the switch that turns libhegel's example database on;
|
|
152
169
|
`database:` only chooses where it writes, and means nothing without a key.
|
|
@@ -162,7 +179,9 @@ Hegel.test(database: "/tmp/wherever") { |tc| tc.draw(integers) }
|
|
|
162
179
|
Give `database_key:` a value unique to the property under test. Two
|
|
163
180
|
`Hegel.test` calls that share a key share one replay scope, so an unrelated
|
|
164
181
|
property can read or overwrite what this one stored. Left at their shared
|
|
165
|
-
default (`nil`, `nil`), a run stores nothing.
|
|
182
|
+
default (`nil`, `nil`), a run stores nothing. With a key and no
|
|
183
|
+
`database:`, the profile chooses the directory, and the `ci` profile turns
|
|
184
|
+
the database off; pass `database:` to store on CI too.
|
|
166
185
|
|
|
167
186
|
`phases:` and `suppress_health_check:` each take an `Array` of `Symbol`, and
|
|
168
187
|
each raises `Hegel::Error` at run time for an empty one. An empty `Array`
|
|
@@ -175,13 +194,13 @@ Hegel.test(phases: []) { |tc| tc.draw(integers) }
|
|
|
175
194
|
# :reuse, :generate, :target, :shrink], got an empty Array"
|
|
176
195
|
```
|
|
177
196
|
|
|
178
|
-
`report_multiple_failures:` defaults to `false`,
|
|
179
|
-
|
|
180
|
-
example and re-raises that failure's own
|
|
181
|
-
intact, so a host framework reports it as
|
|
182
|
-
true` keeps generating afterwards to
|
|
183
|
-
raises `Hegel::Error` naming the count
|
|
184
|
-
exceptions:
|
|
197
|
+
`report_multiple_failures:` defaults to `false`, and is always passed, so a
|
|
198
|
+
profile cannot turn it on by accident. With the default `false`, a run
|
|
199
|
+
stops at its first failing example and re-raises that failure's own
|
|
200
|
+
exception, class and backtrace intact, so a host framework reports it as
|
|
201
|
+
its own. `report_multiple_failures: true` keeps generating afterwards to
|
|
202
|
+
surface other distinct bugs, and then raises `Hegel::Error` naming the count
|
|
203
|
+
instead of any one of the individual exceptions:
|
|
185
204
|
|
|
186
205
|
```ruby
|
|
187
206
|
Hegel.test(test_cases: 10, report_multiple_failures: true, verbosity: :quiet) do |tc|
|
|
@@ -195,11 +214,6 @@ end
|
|
|
195
214
|
# raises Hegel::Error, "Property-based test failed with 2 distinct failures."
|
|
196
215
|
```
|
|
197
216
|
|
|
198
|
-
`stateful_step_count:` bounds how many rules one `Hegel::Stateful.run` call
|
|
199
|
-
applies per test case (see [Stateful Testing](#stateful-testing)); the
|
|
200
|
-
engine documents its own default as 50, and requires the value be at least
|
|
201
|
-
1.
|
|
202
|
-
|
|
203
217
|
`seed:` and `derandomize: true` together make a run reproducible:
|
|
204
218
|
|
|
205
219
|
```ruby
|
|
@@ -310,9 +324,11 @@ body; `assume`/`reject` discard from anywhere in the block.
|
|
|
310
324
|
|
|
311
325
|
`note` records a message for the eventual failure report, interleaved with
|
|
312
326
|
draws in the order both were called. Pass a message directly, or a block.
|
|
313
|
-
The block form is evaluated only on the
|
|
314
|
-
|
|
315
|
-
|
|
327
|
+
The block form is evaluated only on the cases the engine stamps for the
|
|
328
|
+
report: its final replay of each failure, the replays that first confirm
|
|
329
|
+
one, every replay of a `reproduce_failure:` blob, and, for a
|
|
330
|
+
nondeterministic body, the replays that confirm it. So it is the cheaper
|
|
331
|
+
choice when building the message itself costs something. Passing both, or neither, raises `Hegel::Error`:
|
|
316
332
|
|
|
317
333
|
```ruby
|
|
318
334
|
output = StringIO.new
|
|
@@ -419,12 +435,15 @@ Hegel.test(verbosity: :quiet) { |tc| tc.draw(integers(min_value: 10**30, max_val
|
|
|
419
435
|
# => nil
|
|
420
436
|
```
|
|
421
437
|
|
|
422
|
-
### `floats(min_value: nil, max_value: nil, allow_nan:
|
|
438
|
+
### `floats(min_value: nil, max_value: nil, allow_nan: nil, allow_infinity: nil, exclude_min: false, exclude_max: false)`
|
|
439
|
+
|
|
440
|
+
A double in `[min_value, max_value]`, unbounded by default.
|
|
423
441
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
442
|
+
`allow_nan` defaults to `true` when the caller passes neither bound, `false`
|
|
443
|
+
otherwise. `allow_infinity` defaults to `true` when the caller leaves either
|
|
444
|
+
side open, `false` when both bounds are set. hegel-rust and
|
|
445
|
+
hegel-typescript's `floats()` default the same way. Pass `true` or `false` to
|
|
446
|
+
override either (see [Gotchas](#gotchas)).
|
|
428
447
|
|
|
429
448
|
```ruby
|
|
430
449
|
Hegel.test(test_cases: 30, verbosity: :quiet) do |tc|
|
|
@@ -433,12 +452,18 @@ Hegel.test(test_cases: 30, verbosity: :quiet) do |tc|
|
|
|
433
452
|
end
|
|
434
453
|
|
|
435
454
|
Hegel.test(test_cases: 200, verbosity: :quiet) do |tc|
|
|
436
|
-
tc.draw(floats
|
|
455
|
+
tc.draw(floats) # unbounded: draws NaN and both infinities
|
|
456
|
+
tc.draw(floats(allow_nan: false, allow_infinity: false)) # finite doubles only
|
|
437
457
|
end
|
|
438
458
|
```
|
|
439
459
|
|
|
440
|
-
|
|
441
|
-
|
|
460
|
+
Three combinations raise `Hegel::Error` at draw time:
|
|
461
|
+
|
|
462
|
+
- `max_value < min_value`: `"floats: max_value < min_value"`
|
|
463
|
+
- `allow_nan: true` with either bound:
|
|
464
|
+
`"floats: cannot have allow_nan=true with min_value or max_value"`
|
|
465
|
+
- `allow_infinity: true` with both bounds:
|
|
466
|
+
`"floats: cannot have allow_infinity=true with both min_value and max_value"`
|
|
442
467
|
|
|
443
468
|
### `text(min_size: 0, max_size: nil, codec: nil, min_codepoint: nil, max_codepoint: nil)`
|
|
444
469
|
|
|
@@ -469,14 +494,14 @@ expose) are not available in this binding; `codec:`, `min_codepoint:`, and
|
|
|
469
494
|
`max_codepoint:` are the alphabet controls it exposes today.
|
|
470
495
|
|
|
471
496
|
Other generators cover most character-set jobs. These results were measured
|
|
472
|
-
with libhegel 0.
|
|
497
|
+
with libhegel 0.45.0:
|
|
473
498
|
|
|
474
499
|
| Job | Generator | Measured output |
|
|
475
500
|
|---|---|---|
|
|
476
|
-
| Characters to include | `from_regex("[ぁ-んA-Z0-9]{1,5}", fullmatch: true)` | `"
|
|
477
|
-
| Characters to exclude | `from_regex("[^,\\t\\n]{0,6}", fullmatch: true)` | `"
|
|
478
|
-
| A contiguous block | `text(min_codepoint: 0x300, max_codepoint: 0x36F)` | Every character was in U+0300 through U+036F |
|
|
479
|
-
| An arbitrary alphabet | `arrays(sampled_from(%w[ß a İ]), max_size: 4).map(&:join)` | `
|
|
501
|
+
| Characters to include | `from_regex("[ぁ-んA-Z0-9]{1,5}", fullmatch: true)` | `"ぁ"`, `"Hわ9ごう"`; 30 draws all matched the pattern |
|
|
502
|
+
| Characters to exclude | `from_regex("[^,\\t\\n]{0,6}", fullmatch: true)` | `""`, `"\u008FBՔ\u0014C"`; 30 draws contained no comma, tab, or newline |
|
|
503
|
+
| A contiguous block | `text(min_codepoint: 0x300, max_codepoint: 0x36F)` | Every character of 100 draws was in U+0300 through U+036F |
|
|
504
|
+
| An arbitrary alphabet | `arrays(sampled_from(%w[ß a İ]), max_size: 4).map(&:join)` | 30 draws, each built only from `ß`, `a`, and `İ` |
|
|
480
505
|
|
|
481
506
|
The `categories:` option remains an open request. A category such as any `Lu`
|
|
482
507
|
or any `Mn` spans codepoint ranges that a codepoint bound cannot select. A
|
|
@@ -498,7 +523,7 @@ the same setting as `suppress_health_check: [:filter_too_much]`, listed in
|
|
|
498
523
|
|
|
499
524
|
Unconfigured `text` draws from a much wider Unicode range. In 200 draws of
|
|
500
525
|
`text(min_size: 1, max_size: 4)`, the results included emoji, private-use
|
|
501
|
-
characters, and control characters. The highest codepoint was U+
|
|
526
|
+
characters, and control characters. The highest codepoint was U+10A78F. A
|
|
502
527
|
property that accepts any string needs no character-set options.
|
|
503
528
|
|
|
504
529
|
### `arrays(elements, min_size: 0, max_size: nil)`
|
|
@@ -978,8 +1003,28 @@ instance variables directly, and calls a generator method (`integers`,
|
|
|
978
1003
|
surrounding class can once it includes `Hegel::Syntax::Methods`;
|
|
979
1004
|
`Hegel::StateMachine` already includes that module itself.
|
|
980
1005
|
|
|
981
|
-
An invariant runs
|
|
982
|
-
|
|
1006
|
+
An invariant runs before the first rule and after the last. Between rules,
|
|
1007
|
+
the engine samples it with probability 1 / `step_count`, so it runs about
|
|
1008
|
+
once in a test case that runs every step. `invariant(name, always_run:
|
|
1009
|
+
true, &block)` checks it after every rule instead:
|
|
1010
|
+
|
|
1011
|
+
```ruby
|
|
1012
|
+
class CountingMachine < Hegel::StateMachine
|
|
1013
|
+
rule(:step) { |_tc| }
|
|
1014
|
+
invariant(:cheap, always_run: true) { |_tc| }
|
|
1015
|
+
invariant(:expensive) { |_tc| }
|
|
1016
|
+
end
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
`Hegel::Stateful.run(machine, tc, step_count: 50)` takes the most rules one
|
|
1020
|
+
test case runs. It defaults to 50, and the engine rejects a value below 1:
|
|
1021
|
+
|
|
1022
|
+
```ruby
|
|
1023
|
+
Hegel.test(test_cases: 1, verbosity: :quiet) do |tc|
|
|
1024
|
+
Hegel::Stateful.run(CountingMachine.new, tc, step_count: 0)
|
|
1025
|
+
end
|
|
1026
|
+
# raises Hegel::Error, naming HEGEL_E_INVALID_ARG
|
|
1027
|
+
```
|
|
983
1028
|
|
|
984
1029
|
A rule or invariant reports a failure the same way any other test-case body
|
|
985
1030
|
does: raise. This library brings no assertion methods of its own. Include
|
|
@@ -1068,7 +1113,7 @@ output.string
|
|
|
1068
1113
|
```
|
|
1069
1114
|
|
|
1070
1115
|
```
|
|
1071
|
-
Falsified after
|
|
1116
|
+
Falsified after 2 test cases (0 discarded):
|
|
1072
1117
|
|
|
1073
1118
|
Initial invariant check.
|
|
1074
1119
|
Step 1: push
|
|
@@ -1079,7 +1124,7 @@ Falsified after 1 test case (0 discarded):
|
|
|
1079
1124
|
draw_3 = 0
|
|
1080
1125
|
|
|
1081
1126
|
To reproduce this failure, pass the blob below to Hegel.test:
|
|
1082
|
-
reproduce_failure: "
|
|
1127
|
+
reproduce_failure: "AXicjYghEgAACIOkmvz/a9Ww5RE4jqmjeYPCJ30sEXQAaw=="
|
|
1083
1128
|
```
|
|
1084
1129
|
|
|
1085
1130
|
The shrunk report names each step by its rule (`Step 1: push`), and shrinks
|
|
@@ -1181,11 +1226,11 @@ result # => nil
|
|
|
1181
1226
|
|
|
1182
1227
|
## Gotchas
|
|
1183
1228
|
|
|
1184
|
-
1.
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1229
|
+
1. **Float defaults include NaN and infinity when unbounded.** `floats` with
|
|
1230
|
+
no bounds draws `NaN` and both infinities. If your code does not handle
|
|
1231
|
+
them, pass `allow_nan: false, allow_infinity: false` -- but consider
|
|
1232
|
+
whether the code *should* handle them first. A bound turns each one off:
|
|
1233
|
+
NaN needs both bounds absent, an infinity needs one side open.
|
|
1189
1234
|
|
|
1190
1235
|
2. **`integers`'s default range, when a bound is omitted, is still the
|
|
1191
1236
|
signed 64-bit range (`-2**63..(2**63 - 1)`), even though arbitrary
|
|
@@ -1255,7 +1300,6 @@ result # => nil
|
|
|
1255
1300
|
generator's own draw, discards the whole test case.
|
|
1256
1301
|
|
|
1257
1302
|
10. **The suite runs against the real engine on Linux, macOS, and Windows.**
|
|
1258
|
-
Every push to main runs it on Ruby 3.
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
for. Alpine (musl) is the platform this binding has not run on.
|
|
1303
|
+
Every push to main runs it on Ruby 3.4 and 4.0, across Linux x64 and
|
|
1304
|
+
arm64, macOS arm64, and Windows x64 and arm64. Alpine (musl) is the
|
|
1305
|
+
platform this binding has not run on.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hegeltest
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- meganemura
|
|
@@ -60,6 +60,15 @@ files:
|
|
|
60
60
|
- docs/adr/0012-build-a-failure-origin-from-the-callers-own-frame.md
|
|
61
61
|
- docs/adr/0013-bind-libhegel-through-the-ffi-gem.md
|
|
62
62
|
- docs/adr/0014-name-a-drawn-value-only-when-the-draw-is-the-whole-assigned-value.md
|
|
63
|
+
- docs/adr/0015-follow-the-hegeldev-interface-and-take-changes-upstream-first.md
|
|
64
|
+
- docs/adr/0016-run-mutation-testing-with-mutineer.md
|
|
65
|
+
- docs/adr/0017-raise-the-ruby-floor-to-3-4.md
|
|
66
|
+
- docs/adr/0018-gate-mutation-testing-on-a-committed-baseline.md
|
|
67
|
+
- docs/adr/0019-report-failures-from-the-cases-the-engine-stamps.md
|
|
68
|
+
- docs/adr/0020-derive-span-labels-from-generator-names.md
|
|
69
|
+
- docs/adr/0021-run-a-state-machine-in-rounds-with-its-own-step-count.md
|
|
70
|
+
- docs/adr/0022-keep-microsecond-times-over-a-nanosecond-engine.md
|
|
71
|
+
- docs/adr/0023-leave-unset-settings-to-the-engines-profile.md
|
|
63
72
|
- docs/architecture.md
|
|
64
73
|
- lib/hegel.rb
|
|
65
74
|
- lib/hegel/draw_name.rb
|
|
@@ -101,7 +110,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
101
110
|
requirements:
|
|
102
111
|
- - ">="
|
|
103
112
|
- !ruby/object:Gem::Version
|
|
104
|
-
version: 3.
|
|
113
|
+
version: 3.4.0
|
|
105
114
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
106
115
|
requirements:
|
|
107
116
|
- - ">="
|