hegeltest 0.1.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 +7 -0
- data/CHANGELOG.md +22 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +264 -0
- data/Rakefile +19 -0
- data/docs/README.md +25 -0
- data/docs/adr/0001-bind-libhegel-through-fiddle.md +54 -0
- data/docs/adr/0002-ship-one-prebuilt-engine-per-platform-specific-gem.md +48 -0
- data/docs/adr/0003-publish-as-hegeltest-require-as-hegel.md +39 -0
- data/docs/adr/0004-expose-generators-through-a-mixin-with-keyword-options.md +42 -0
- data/docs/adr/0005-name-drawn-values-from-the-callers-source-with-prism.md +40 -0
- data/docs/adr/0006-verify-the-binding-in-seven-layers-with-full-coverage.md +51 -0
- data/docs/adr/0007-ship-a-thin-ruby-skill-shaped-for-donation.md +56 -0
- data/docs/adr/0008-revisit-the-binding-after-milestone-c-on-measurement.md +81 -0
- data/docs/adr/0009-turn-the-example-database-on-with-a-key.md +89 -0
- data/docs/adr/0010-declare-stateful-rules-with-a-class-macro.md +113 -0
- data/docs/adr/0011-let-the-test-case-own-every-pool-drawn-from-it.md +83 -0
- data/docs/adr/0012-build-a-failure-origin-from-the-callers-own-frame.md +72 -0
- data/docs/adr/0013-bind-libhegel-through-the-ffi-gem.md +102 -0
- data/docs/architecture.md +182 -0
- data/lib/hegel/draw_name.rb +109 -0
- data/lib/hegel/errors.rb +47 -0
- data/lib/hegel/generator.rb +98 -0
- data/lib/hegel/generators.rb +865 -0
- data/lib/hegel/lib_hegel/real.rb +1149 -0
- data/lib/hegel/lib_hegel.rb +269 -0
- data/lib/hegel/libhegel_version.rb +9 -0
- data/lib/hegel/locate.rb +188 -0
- data/lib/hegel/report.rb +87 -0
- data/lib/hegel/runner.rb +464 -0
- data/lib/hegel/settings.rb +164 -0
- data/lib/hegel/state_machine.rb +89 -0
- data/lib/hegel/stateful/pool.rb +111 -0
- data/lib/hegel/stateful.rb +120 -0
- data/lib/hegel/syntax/methods.rb +173 -0
- data/lib/hegel/test_case.rb +523 -0
- data/lib/hegel/version.rb +5 -0
- data/lib/hegel.rb +92 -0
- data/lib/hegeltest.rb +7 -0
- data/lib/tasks/libhegel.rake +112 -0
- data/lib/tasks/platform_gems.rake +111 -0
- data/sig/hegel.rbs +563 -0
- data/skills/hegel-ruby/SKILL.md +30 -0
- data/skills/hegel-ruby/references/ruby/reference.md +1210 -0
- metadata +113 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "syntax/methods"
|
|
5
|
+
|
|
6
|
+
module Hegel
|
|
7
|
+
# Base class for a stateful (model-based) test's machine: a subclass
|
|
8
|
+
# declares its actions with the class-level #rule and #invariant macros,
|
|
9
|
+
# then Hegel::Stateful.run drives one instance of it inside an ordinary
|
|
10
|
+
# Hegel.test block. See docs/adr/0010 for the declared shape (a class with
|
|
11
|
+
# macros, not a method-naming convention or an instance-built list) and
|
|
12
|
+
# the reasons behind it.
|
|
13
|
+
#
|
|
14
|
+
# Only declaration lives here. Hegel::Stateful.run owns the loop that
|
|
15
|
+
# calls the declared blocks, the same split hegel-rust draws between its
|
|
16
|
+
# StateMachine trait (rules()/invariants()) and its own free function
|
|
17
|
+
# `run`.
|
|
18
|
+
class StateMachine
|
|
19
|
+
# So a rule or invariant block can call a generator method (integers,
|
|
20
|
+
# arrays, and so on) bare, the same way a Hegel.test block already can
|
|
21
|
+
# via the caller's own include -- see docs/adr/0010's own reasoning for
|
|
22
|
+
# why a machine needs this itself rather than inheriting its test
|
|
23
|
+
# class's.
|
|
24
|
+
include Syntax::Methods
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# Declares a rule named +name+: an action the engine may pick to run
|
|
28
|
+
# at any step. +block+ runs via #instance_exec against the machine
|
|
29
|
+
# instance being tested, and is handed the running Hegel::TestCase as
|
|
30
|
+
# its one argument -- ignored if the block declares no parameter, the
|
|
31
|
+
# ordinary Ruby block rule.
|
|
32
|
+
def rule(name, &block)
|
|
33
|
+
declare(:@rules, "rule", name, block)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Declares an invariant named +name+, checked once before the first
|
|
37
|
+
# rule runs and again after every rule that completes without its own
|
|
38
|
+
# assumption failing. Same block/argument contract as #rule.
|
|
39
|
+
def invariant(name, &block)
|
|
40
|
+
declare(:@invariants, "invariant", name, block)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# name => block, in declaration order, this class's own declarations
|
|
44
|
+
# merged over its ancestors'. Hegel::Stateful.run reads this directly
|
|
45
|
+
# to build the ordered rule-name list libhegel indexes by position.
|
|
46
|
+
def rule_definitions
|
|
47
|
+
merged_definitions(:@rules, :rule_definitions)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The invariant analogue of #rule_definitions.
|
|
51
|
+
def invariant_definitions
|
|
52
|
+
merged_definitions(:@invariants, :invariant_definitions)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# Adds +name+ (stringified, so a Symbol and the same-named String
|
|
58
|
+
# collide) to the table at +ivar+, raising Hegel::Error when *this*
|
|
59
|
+
# class already declares one by that name -- the disappearing-rule
|
|
60
|
+
# failure docs/adr/0010 exists to turn into a raise instead. A name
|
|
61
|
+
# only a superclass declared is not a collision here: #declare never
|
|
62
|
+
# reads an ancestor's table, so a subclass re-declaring an inherited
|
|
63
|
+
# name is the ordinary "redefine a method" case the ADR calls out,
|
|
64
|
+
# not this one.
|
|
65
|
+
def declare(ivar, kind, name, block)
|
|
66
|
+
table = instance_variable_get(ivar) || instance_variable_set(ivar, {})
|
|
67
|
+
name = name.to_s
|
|
68
|
+
raise Hegel::Error, "hegel: #{kind} #{name.inspect} is already declared on #{self}" if table.key?(name)
|
|
69
|
+
|
|
70
|
+
table[name] = block
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Shared by #rule_definitions/#invariant_definitions: this class's own
|
|
74
|
+
# table layered over its superclass's already-merged one via
|
|
75
|
+
# Hash#merge, so a name declared on both keeps the position it first
|
|
76
|
+
# held (Hash#merge's own behaviour for a key present in both operands)
|
|
77
|
+
# while every new name from this class is appended -- together, the
|
|
78
|
+
# declaration order the whole ancestor chain built. Recursion stops at
|
|
79
|
+
# the first class that does not respond to +reader+ (Object, past
|
|
80
|
+
# Hegel::StateMachine's own top), rather than comparing against
|
|
81
|
+
# StateMachine directly, so nothing here hard-codes this one class as
|
|
82
|
+
# the root.
|
|
83
|
+
def merged_definitions(ivar, reader)
|
|
84
|
+
inherited = superclass.respond_to?(reader) ? superclass.public_send(reader) : {}
|
|
85
|
+
inherited.merge(instance_variable_get(ivar) || {})
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../generator"
|
|
4
|
+
|
|
5
|
+
module Hegel
|
|
6
|
+
module Stateful
|
|
7
|
+
# A pool of previously generated values, for a later stateful rule to
|
|
8
|
+
# draw one back out. Build one from the running test case, inside the
|
|
9
|
+
# machine's own constructor:
|
|
10
|
+
#
|
|
11
|
+
# Hegel::Stateful::Pool.new(tc)
|
|
12
|
+
#
|
|
13
|
+
# #add records a value under a fresh variable id (hegel_pool_add); #size
|
|
14
|
+
# and #empty? read the Ruby-side count directly. Drawing goes through the
|
|
15
|
+
# two generators below, not through this class's own storage, so a
|
|
16
|
+
# chosen id is drawn (and shrunk, and recorded in a failure report) the
|
|
17
|
+
# same way any other value is: #values_reusable leaves the drawn value in
|
|
18
|
+
# the pool, #values_consumed removes it. hegel-rust's own Pool<T>
|
|
19
|
+
# (src/stateful.rs) keeps the same three-way split between the engine's
|
|
20
|
+
# variable-id choice, this class's own id-to-value map, and the two
|
|
21
|
+
# generators.
|
|
22
|
+
#
|
|
23
|
+
# A caller never frees a pool. docs/adr/0011 has the reason: #initialize
|
|
24
|
+
# opens the native handle through Hegel::TestCase#new_pool, which records
|
|
25
|
+
# it on +tc+ itself, and Hegel::Runner frees every pool a test case
|
|
26
|
+
# recorded once that test case is done -- the same "the test case owns
|
|
27
|
+
# what it opened" split this library already uses for a state-machine
|
|
28
|
+
# handle.
|
|
29
|
+
class Pool
|
|
30
|
+
def initialize(tc)
|
|
31
|
+
@tc = tc
|
|
32
|
+
@pool = tc.new_pool
|
|
33
|
+
@values = {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Number of values currently in the pool.
|
|
37
|
+
def size
|
|
38
|
+
@values.size
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# True when no values are in the pool.
|
|
42
|
+
def empty?
|
|
43
|
+
@values.empty?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Records +value+ under a fresh variable id from hegel_pool_add.
|
|
47
|
+
# Returns self, Set#add's own contract -- hegel-rust's own Pool::add
|
|
48
|
+
# returns nothing instead, since Rust has no builder-chaining idiom for
|
|
49
|
+
# this method to match.
|
|
50
|
+
def add(value)
|
|
51
|
+
variable_id = @tc.pool_add(@pool)
|
|
52
|
+
@values[variable_id] = value
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# A Hegel::Generator over this pool's values: drawing it leaves the
|
|
57
|
+
# chosen value in place, so the same value can be drawn again.
|
|
58
|
+
def values_reusable
|
|
59
|
+
ValuesReusable.new(@pool, @values)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# A Hegel::Generator over this pool's values: drawing it removes the
|
|
63
|
+
# chosen value, so it is never drawn again.
|
|
64
|
+
def values_consumed
|
|
65
|
+
ValuesConsumed.new(@pool, @values)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Hegel::Generator returned by Pool#values_reusable. Left un-namespaced
|
|
69
|
+
# under Hegel::Generators, the same way Hegel::Generator::Mapped and
|
|
70
|
+
# ::Filtered are: reached only through Pool#values_reusable, not part
|
|
71
|
+
# of this library's own public generator vocabulary.
|
|
72
|
+
class ValuesReusable < Generator
|
|
73
|
+
def initialize(pool, values)
|
|
74
|
+
super()
|
|
75
|
+
@pool = pool
|
|
76
|
+
@values = values
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Does not check @values.empty? before drawing: hegel_pool_generate
|
|
80
|
+
# already answers HEGEL_E_ASSUME for an empty pool, which
|
|
81
|
+
# Hegel::LibHegel.check! already translates to Hegel::AssumeFailed --
|
|
82
|
+
# the same translation every other assumption failure gets.
|
|
83
|
+
# hegel-rust's own ValuesReusable calls tc.assume ahead of its own
|
|
84
|
+
# pool_generate call (src/stateful.rs). Here the engine's answer is
|
|
85
|
+
# the single path, so the empty case has one place to change.
|
|
86
|
+
def do_draw(tc)
|
|
87
|
+
variable_id = tc.pool_generate(@pool, false)
|
|
88
|
+
@values.fetch(variable_id)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Hegel::Generator returned by Pool#values_consumed. Same reasoning as
|
|
93
|
+
# ValuesReusable above for staying un-namespaced and for not
|
|
94
|
+
# pre-checking emptiness.
|
|
95
|
+
class ValuesConsumed < Generator
|
|
96
|
+
def initialize(pool, values)
|
|
97
|
+
super()
|
|
98
|
+
@pool = pool
|
|
99
|
+
@values = values
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def do_draw(tc)
|
|
103
|
+
variable_id = tc.pool_generate(@pool, true)
|
|
104
|
+
value = @values.fetch(variable_id)
|
|
105
|
+
@values.delete(variable_id)
|
|
106
|
+
value
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "lib_hegel"
|
|
5
|
+
require_relative "stateful/pool"
|
|
6
|
+
|
|
7
|
+
module Hegel
|
|
8
|
+
# Runs one stateful (model-based) test: drives libhegel's own state-machine
|
|
9
|
+
# loop (hegel_new_state_machine and the three calls that go with it) against
|
|
10
|
+
# a Hegel::StateMachine instance's declared rules and invariants. Call it
|
|
11
|
+
# from inside an ordinary Hegel.test block, the same way any other draw
|
|
12
|
+
# happens:
|
|
13
|
+
#
|
|
14
|
+
# Hegel.test { |tc| Hegel::Stateful.run(StackMachine.new, tc) }
|
|
15
|
+
#
|
|
16
|
+
# A module function, not a Hegel::StateMachine instance method, so that
|
|
17
|
+
# class stays limited to declaration -- the same split hegel-rust's own
|
|
18
|
+
# src/stateful.rs draws between the StateMachine trait and its free
|
|
19
|
+
# function `run`, which this module's own #run is ported from; see that
|
|
20
|
+
# file's comments for the reasoning behind the ordering below.
|
|
21
|
+
module Stateful
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# +machine+ is a Hegel::StateMachine instance; +tc+ the running
|
|
25
|
+
# Hegel::TestCase.
|
|
26
|
+
#
|
|
27
|
+
# Raises Hegel::Error before making any libhegel call when +machine+
|
|
28
|
+
# declares no rules: hegel.h documents hegel_new_state_machine's
|
|
29
|
+
# rule_names as required to be non-empty, and there is nothing useful to
|
|
30
|
+
# run without one.
|
|
31
|
+
def run(machine, tc)
|
|
32
|
+
rules = machine.class.rule_definitions
|
|
33
|
+
raise Hegel::Error, "hegel: #{machine.class} has no rules; declare at least one with `rule`" if rules.empty?
|
|
34
|
+
|
|
35
|
+
invariants = machine.class.invariant_definitions
|
|
36
|
+
rule_names = rules.keys
|
|
37
|
+
state_machine = tc.new_state_machine(rule_names, invariants.keys)
|
|
38
|
+
begin
|
|
39
|
+
tc.note { "Initial invariant check." }
|
|
40
|
+
run_invariants(machine, invariants, tc)
|
|
41
|
+
drive(machine, rules, rule_names, invariants, state_machine, tc)
|
|
42
|
+
ensure
|
|
43
|
+
tc.state_machine_free(state_machine)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Repeatedly asks +state_machine+ for the next rule to run and applies
|
|
48
|
+
# it, until libhegel reports the step budget for this test case spent.
|
|
49
|
+
#
|
|
50
|
+
# Measured against libhegel 0.32.5, unseeded, on the capacity-2 stack
|
|
51
|
+
# shrink-quality test below (test/hegel/test_stateful.rb,
|
|
52
|
+
# test_stateful_run_shrinks_to_the_minimal_step_count_that_breaks_the_
|
|
53
|
+
# invariant): closing the HEGEL_LABEL_STATEFUL_RULE span with
|
|
54
|
+
# stop_span(discard: false) on the DONE branch, right before breaking
|
|
55
|
+
# out of this loop, and leaving it open instead (matching hegel-rust's
|
|
56
|
+
# own `run`, which never closes that last span) both shrink the same
|
|
57
|
+
# failure to the same 3-step counterexample every time, 20 runs each.
|
|
58
|
+
# This keeps hegel-rust's own choice -- an unclosed span at DONE --
|
|
59
|
+
# since nothing measured favours the extra stop_span call.
|
|
60
|
+
def drive(machine, rules, rule_names, invariants, state_machine, tc)
|
|
61
|
+
steps_attempted = 0
|
|
62
|
+
loop do
|
|
63
|
+
tc.start_span(LibHegel::HEGEL_LABEL_STATEFUL_RULE)
|
|
64
|
+
rule_index = tc.state_machine_next_rule(state_machine)
|
|
65
|
+
break if rule_index == LibHegel::HEGEL_STATE_MACHINE_DONE
|
|
66
|
+
|
|
67
|
+
name = rule_names[rule_index]
|
|
68
|
+
steps_attempted += 1
|
|
69
|
+
tc.note { "Step #{steps_attempted}: #{name}" }
|
|
70
|
+
apply_rule(machine, rules.fetch(name), invariants, state_machine, tc)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# standard:disable Lint/RescueException -- deliberate, the same reason
|
|
75
|
+
# Hegel::Runner.classify's own `rescue Exception` is: Hegel::AssumeFailed
|
|
76
|
+
# and Hegel::StopTest both descend from Exception, not StandardError, so
|
|
77
|
+
# only `rescue Exception` sees every path a rule can take. Every branch
|
|
78
|
+
# other than AssumeFailed re-raises what it caught unchanged -- this
|
|
79
|
+
# never reclassifies an exception or swallows one, it only guarantees
|
|
80
|
+
# the span closes first, so a half-applied rule is never left mid-span
|
|
81
|
+
# when the exception unwinds past this method.
|
|
82
|
+
#
|
|
83
|
+
# Hegel::FATAL_EXCEPTIONS goes first and closes no span. They say the
|
|
84
|
+
# process is ending, so the span has no reader left to matter to, and
|
|
85
|
+
# answering a NoMemoryError with another native call is the wrong move.
|
|
86
|
+
# A rule is the one place in this library where a fatal exception is
|
|
87
|
+
# raised inside an open span, so this is where that ordering has to be
|
|
88
|
+
# written.
|
|
89
|
+
#
|
|
90
|
+
# tc.assume(false) inside a rule is not the same event as one raised
|
|
91
|
+
# directly inside a Hegel.test block: it rejects only this rule (told to
|
|
92
|
+
# libhegel via #state_machine_rule_rejected, so the rejected attempt
|
|
93
|
+
# does not count toward the step budget) and the loop keeps going, where
|
|
94
|
+
# Hegel::Runner.classify's own AssumeFailed handling discards the whole
|
|
95
|
+
# test case. Hegel::Runner.classify never sees this one: it is caught
|
|
96
|
+
# and handled right here.
|
|
97
|
+
def apply_rule(machine, block, invariants, state_machine, tc)
|
|
98
|
+
machine.instance_exec(tc, &block)
|
|
99
|
+
rescue *Hegel::FATAL_EXCEPTIONS
|
|
100
|
+
raise
|
|
101
|
+
rescue Hegel::AssumeFailed
|
|
102
|
+
tc.state_machine_rule_rejected(state_machine)
|
|
103
|
+
tc.stop_span(discard: true)
|
|
104
|
+
tc.note { "Rule stopped early due to violated assumption." }
|
|
105
|
+
rescue Exception
|
|
106
|
+
tc.stop_span(discard: false)
|
|
107
|
+
raise
|
|
108
|
+
else
|
|
109
|
+
tc.stop_span(discard: false)
|
|
110
|
+
run_invariants(machine, invariants, tc)
|
|
111
|
+
end
|
|
112
|
+
# standard:enable Lint/RescueException
|
|
113
|
+
|
|
114
|
+
# Runs every invariant, in declaration order, via #instance_exec -- same
|
|
115
|
+
# argument contract as a rule block (Hegel::StateMachine.invariant).
|
|
116
|
+
def run_invariants(machine, invariants, tc)
|
|
117
|
+
invariants.each_value { |block| machine.instance_exec(tc, &block) }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../generators"
|
|
4
|
+
|
|
5
|
+
module Hegel
|
|
6
|
+
module Syntax
|
|
7
|
+
# The generator-constructing methods a caller can include to call bare,
|
|
8
|
+
# matching FactoryBot::Syntax::Methods's shape (see docs/adr/0004):
|
|
9
|
+
#
|
|
10
|
+
# RSpec.configure { |config| config.include Hegel::Syntax::Methods }
|
|
11
|
+
#
|
|
12
|
+
# This is the one place any of these methods is defined.
|
|
13
|
+
# Hegel::Generators.<name> reaches the same methods without an
|
|
14
|
+
# include, via Hegel::Generators extending this module below.
|
|
15
|
+
module Methods
|
|
16
|
+
# A boolean, true with probability +p+.
|
|
17
|
+
def booleans(p: 0.5)
|
|
18
|
+
Generators::BooleanGenerator.new(p: p)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# An integer in [min_value, max_value], defaulting to the full
|
|
22
|
+
# 64-bit range when either bound is omitted.
|
|
23
|
+
def integers(min_value: nil, max_value: nil)
|
|
24
|
+
Generators::IntegerGenerator.new(min_value: min_value, max_value: max_value)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# A double in [min_value, max_value]. allow_nan and allow_infinity
|
|
28
|
+
# both default to false.
|
|
29
|
+
def floats(min_value: nil, max_value: nil, allow_nan: false, allow_infinity: false, exclude_min: false,
|
|
30
|
+
exclude_max: false)
|
|
31
|
+
Generators::FloatGenerator.new(
|
|
32
|
+
min_value: min_value, max_value: max_value, allow_nan: allow_nan, allow_infinity: allow_infinity,
|
|
33
|
+
exclude_min: exclude_min, exclude_max: exclude_max
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# A Unicode string of [min_size, max_size] characters.
|
|
38
|
+
def text(min_size: 0, max_size: nil, codec: nil, min_codepoint: nil, max_codepoint: nil)
|
|
39
|
+
Generators::TextGenerator.new(
|
|
40
|
+
min_size: min_size, max_size: max_size, codec: codec,
|
|
41
|
+
min_codepoint: min_codepoint, max_codepoint: max_codepoint
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# An Array of values from +elements+, with [min_size, max_size]
|
|
46
|
+
# entries.
|
|
47
|
+
def arrays(elements, min_size: 0, max_size: nil)
|
|
48
|
+
Generators::ArrayGenerator.new(elements, min_size: min_size, max_size: max_size)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Always +value+, drawing nothing.
|
|
52
|
+
def just(value)
|
|
53
|
+
Generators::JustGenerator.new(value)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# One element of +collection+, picked at random.
|
|
57
|
+
def sampled_from(collection)
|
|
58
|
+
Generators::SampledFromGenerator.new(collection)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A value drawn from one of +generators+, picked at random.
|
|
62
|
+
def one_of(*generators)
|
|
63
|
+
Generators::OneOfGenerator.new(generators)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A value drawn from +generator+ half the time, nil the other half.
|
|
67
|
+
def optional(generator)
|
|
68
|
+
Generators::OptionalGenerator.new(generator)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# An Array holding one value drawn from each of +generators+, in
|
|
72
|
+
# order (Ruby has no tuple type; see docs/adr/0004).
|
|
73
|
+
def tuples(*generators)
|
|
74
|
+
Generators::TupleGenerator.new(generators)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# A Set of values from +elements+, with [min_size, max_size] entries.
|
|
78
|
+
def sets(elements, min_size: 0, max_size: nil)
|
|
79
|
+
Generators::SetGenerator.new(elements, min_size: min_size, max_size: max_size)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# A Hash whose keys are drawn from +keys+ and values from +values+,
|
|
83
|
+
# with [min_size, max_size] entries.
|
|
84
|
+
def hashes(keys, values, min_size: 0, max_size: nil)
|
|
85
|
+
Generators::HashGenerator.new(keys, values, min_size: min_size, max_size: max_size)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# A String of exactly one character, sharing #text's own alphabet
|
|
89
|
+
# options.
|
|
90
|
+
def characters(codec: nil, min_codepoint: nil, max_codepoint: nil)
|
|
91
|
+
Generators::CharactersGenerator.new(codec: codec, min_codepoint: min_codepoint, max_codepoint: max_codepoint)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# A byte String of [min_size, max_size] bytes.
|
|
95
|
+
def binary(min_size: 0, max_size: nil)
|
|
96
|
+
Generators::BinaryGenerator.new(min_size: min_size, max_size: max_size)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A String matching +pattern+ (Python `re` syntax, not Ruby's
|
|
100
|
+
# Regexp syntax; see Generators::FromRegexGenerator). fullmatch
|
|
101
|
+
# requires the whole string to match, not just contain a match.
|
|
102
|
+
def from_regex(pattern, fullmatch: false)
|
|
103
|
+
Generators::FromRegexGenerator.new(pattern, fullmatch: fullmatch)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# An RFC 5321/5322 email address String.
|
|
107
|
+
def emails
|
|
108
|
+
Generators::EmailsGenerator.new
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# An RFC 3986 http/https URL String.
|
|
112
|
+
def urls
|
|
113
|
+
Generators::UrlsGenerator.new
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# A fully-qualified domain name String of at most +max_length+
|
|
117
|
+
# characters.
|
|
118
|
+
def domains(max_length: 255)
|
|
119
|
+
Generators::DomainsGenerator.new(max_length: max_length)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# An IPAddr, v4 or v6 depending on +v4+/+v6+ (see
|
|
123
|
+
# Generators::IpAddressesGenerator).
|
|
124
|
+
def ip_addresses(v4: true, v6: true)
|
|
125
|
+
Generators::IpAddressesGenerator.new(v4: v4, v6: v6)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# A UUID String in the standard 8-4-4-4-12 hex form. version: nil (the
|
|
129
|
+
# default) draws uniform random bits except the nil UUID; an explicit
|
|
130
|
+
# version forces the RFC 4122 version and variant nibbles (see
|
|
131
|
+
# Generators::UuidsGenerator).
|
|
132
|
+
def uuids(version: nil)
|
|
133
|
+
Generators::UuidsGenerator.new(version: version)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# A proleptic Gregorian calendar Date in [min_value, max_value],
|
|
137
|
+
# defaulting to year 1 through year 9999.
|
|
138
|
+
def dates(min_value: nil, max_value: nil)
|
|
139
|
+
Generators::DatesGenerator.new(min_value: min_value, max_value: max_value)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# A time of day String, "HH:MM:SS.ffffff", in [min_value, max_value]
|
|
143
|
+
# (also "HH:MM:SS.ffffff" Strings), defaulting to the full day.
|
|
144
|
+
def times(min_value: nil, max_value: nil)
|
|
145
|
+
Generators::TimesGenerator.new(min_value: min_value, max_value: max_value)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# A naive (no timezone) Time in [min_value, max_value], defaulting to
|
|
149
|
+
# year 1 through year 9999 (see Generators::DatetimesGenerator).
|
|
150
|
+
def datetimes(min_value: nil, max_value: nil)
|
|
151
|
+
Generators::DatetimesGenerator.new(min_value: min_value, max_value: max_value)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# A value built from imperative code: +block+ receives a draw
|
|
155
|
+
# surface and may call #draw on it any number of times to assemble
|
|
156
|
+
# one value (see Generators::CompositeGenerator).
|
|
157
|
+
def composite(&block)
|
|
158
|
+
Generators::CompositeGenerator.new(&block)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# A forward reference to a generator whose definition is supplied
|
|
162
|
+
# later via #set, enabling self-recursive and mutually recursive
|
|
163
|
+
# generators (see Generators::DeferredGenerator).
|
|
164
|
+
def deferred
|
|
165
|
+
Generators::DeferredGenerator.new
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
module Generators
|
|
171
|
+
extend Syntax::Methods
|
|
172
|
+
end
|
|
173
|
+
end
|