factory_hoist 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/LICENSE.txt +21 -0
- data/README.md +125 -0
- data/Rakefile +8 -0
- data/bench/fast_build.rb +37 -0
- data/bench/phase0_synthetic.rb +108 -0
- data/docs/adversarial-audit.md +50 -0
- data/docs/phase0-synthetic.md +19 -0
- data/exe/factory_hoist +15 -0
- data/lib/factory_hoist/advisor.rb +67 -0
- data/lib/factory_hoist/bulk_writer.rb +44 -0
- data/lib/factory_hoist/compatibility.rb +18 -0
- data/lib/factory_hoist/configuration.rb +14 -0
- data/lib/factory_hoist/database_snapshot.rb +54 -0
- data/lib/factory_hoist/deep_copy.rb +11 -0
- data/lib/factory_hoist/definition.rb +27 -0
- data/lib/factory_hoist/fast_build.rb +211 -0
- data/lib/factory_hoist/minitest.rb +78 -0
- data/lib/factory_hoist/parallel_database.rb +87 -0
- data/lib/factory_hoist/pcg32.rb +76 -0
- data/lib/factory_hoist/rspec.rb +37 -0
- data/lib/factory_hoist/runtime.rb +326 -0
- data/lib/factory_hoist/scheduler.rb +133 -0
- data/lib/factory_hoist/stats.rb +52 -0
- data/lib/factory_hoist/transaction.rb +90 -0
- data/lib/factory_hoist/version.rb +5 -0
- data/lib/factory_hoist.rb +151 -0
- data/script/verify_postgresql +102 -0
- data/script/verify_postgresql_clone +61 -0
- metadata +130 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "ripper"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
module FactoryHoist
|
|
8
|
+
module FastBuild
|
|
9
|
+
FALLBACK = Object.new.freeze
|
|
10
|
+
@compiled = {}
|
|
11
|
+
@generation = 0
|
|
12
|
+
@pending = {}
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def call(name, traits, overrides)
|
|
17
|
+
return FALLBACK unless traits.empty?
|
|
18
|
+
|
|
19
|
+
key = name.is_a?(Symbol) ? name : name.to_sym
|
|
20
|
+
cached = @compiled[key]
|
|
21
|
+
evaluator = cached ? cached[:evaluator] : compile(key)
|
|
22
|
+
if evaluator && !overrides.empty?
|
|
23
|
+
override_names = overrides.keys.map(&:to_sym)
|
|
24
|
+
aliases = evaluator.factory_ir[:attributes].any? do |attribute|
|
|
25
|
+
override_names.any? { |override| attribute.name != override && attribute.alias_for?(override) }
|
|
26
|
+
end
|
|
27
|
+
return FALLBACK if aliases
|
|
28
|
+
end
|
|
29
|
+
evaluator ? evaluator.new(overrides).build : FALLBACK
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def compile(name)
|
|
33
|
+
factory = ::FactoryBot::Internal.factory_by_name(name)
|
|
34
|
+
cached = @compiled[name.to_sym]
|
|
35
|
+
return cached[:evaluator] if cached && cached[:factory].equal?(factory)
|
|
36
|
+
|
|
37
|
+
@mutex.synchronize do
|
|
38
|
+
cached = @compiled[name.to_sym]
|
|
39
|
+
return cached[:evaluator] if cached && cached[:factory].equal?(factory)
|
|
40
|
+
|
|
41
|
+
ir = build_ir(factory)
|
|
42
|
+
return unless ir
|
|
43
|
+
|
|
44
|
+
install_reload_hook
|
|
45
|
+
@generation += 1
|
|
46
|
+
safe_name = name.to_s.gsub(/[^a-zA-Z0-9_-]/, "_")[0, 80]
|
|
47
|
+
token = "#{safe_name}_#{Process.pid}_#{factory.object_id}_#{@generation}"
|
|
48
|
+
@pending[token] = ir
|
|
49
|
+
path = source_path(token)
|
|
50
|
+
ir[:source_path] = path
|
|
51
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
52
|
+
File.write(path, generated_source(token, ir))
|
|
53
|
+
require path
|
|
54
|
+
@compiled.fetch(name.to_sym).fetch(:evaluator)
|
|
55
|
+
end
|
|
56
|
+
rescue KeyError, NameError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def install(token, &builder)
|
|
61
|
+
ir = @pending.delete(token)
|
|
62
|
+
evaluator = Class.new(Evaluator)
|
|
63
|
+
ir[:attributes].each do |attribute|
|
|
64
|
+
raw_name = :"__factory_hoist_#{attribute.name}"
|
|
65
|
+
evaluator.define_method(raw_name, &attribute.to_proc)
|
|
66
|
+
end
|
|
67
|
+
evaluator.class_eval(&builder)
|
|
68
|
+
evaluator.factory_ir = ir
|
|
69
|
+
@compiled[ir[:name]] = {
|
|
70
|
+
factory: ir[:factory], evaluator: evaluator, source_path: ir[:source_path]
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def reset!
|
|
75
|
+
@mutex.synchronize do
|
|
76
|
+
@compiled.clear
|
|
77
|
+
@pending.clear
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def reload!
|
|
82
|
+
@mutex.synchronize do
|
|
83
|
+
@compiled.each_value { |entry| entry[:evaluator].factory_ir.delete(:klass) }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def source_path(token)
|
|
88
|
+
File.join(Dir.tmpdir, "factory_hoist", "#{token}.rb")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def compiled_source(name)
|
|
92
|
+
@compiled.dig(name.to_sym, :source_path)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def install_reload_hook
|
|
98
|
+
return if @reload_hook_installed
|
|
99
|
+
return unless defined?(::ActiveSupport::Reloader)
|
|
100
|
+
|
|
101
|
+
::ActiveSupport::Reloader.to_prepare { reload! }
|
|
102
|
+
@reload_hook_installed = true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def generated_source(token, ir)
|
|
106
|
+
assignments = ir[:assigned].map { |name| " object.#{name} = #{name}" }.join("\n")
|
|
107
|
+
known = (ir[:assigned] | ir[:ignored]).map(&:inspect).join(", ")
|
|
108
|
+
readers = ir[:attributes].map do |attribute|
|
|
109
|
+
name = attribute.name
|
|
110
|
+
<<~RUBY
|
|
111
|
+
def #{name}
|
|
112
|
+
return @cache[:#{name}] if @cache.key?(:#{name})
|
|
113
|
+
@cache[:#{name}] = __factory_hoist_#{name}
|
|
114
|
+
end
|
|
115
|
+
RUBY
|
|
116
|
+
end.join("\n")
|
|
117
|
+
<<~RUBY
|
|
118
|
+
FactoryHoist::FastBuild.install(#{token.dump}) do
|
|
119
|
+
#{readers}
|
|
120
|
+
def build
|
|
121
|
+
object = @instance = build_class.new
|
|
122
|
+
#{assignments}
|
|
123
|
+
unless @overrides.empty?
|
|
124
|
+
@overrides.each do |name, value|
|
|
125
|
+
object.public_send(:"\#{name}=", value) unless [#{known}].include?(name)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
object
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
RUBY
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def build_ir(factory)
|
|
135
|
+
factory.compile
|
|
136
|
+
definition = factory.definition
|
|
137
|
+
return unless definition.callbacks.empty? && definition.constructor.nil?
|
|
138
|
+
|
|
139
|
+
attributes = factory.send(:attributes).to_a
|
|
140
|
+
klass = factory.build_class
|
|
141
|
+
assigned = attributes.reject(&:ignored).map(&:name)
|
|
142
|
+
names = attributes.map(&:name)
|
|
143
|
+
reserved = Evaluator.instance_methods(true) | Evaluator.private_instance_methods(true) | [:build]
|
|
144
|
+
required_arguments = klass.instance_method(:initialize).parameters.any? do |type, _name|
|
|
145
|
+
%i[req keyreq].include?(type)
|
|
146
|
+
end
|
|
147
|
+
invalid_name = names.any? do |name|
|
|
148
|
+
!name.match?(/\A[a-z_]\w*\z/) || reserved.include?(name) || Ripper.lex(name.to_s).first[1] == :on_kw
|
|
149
|
+
end
|
|
150
|
+
return if required_arguments || !klass.name || invalid_name
|
|
151
|
+
|
|
152
|
+
{
|
|
153
|
+
name: factory.name,
|
|
154
|
+
factory: factory,
|
|
155
|
+
class_name: klass.name,
|
|
156
|
+
klass: klass,
|
|
157
|
+
attributes: attributes,
|
|
158
|
+
assigned: assigned,
|
|
159
|
+
ignored: attributes.select(&:ignored).map(&:name)
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
class Evaluator
|
|
165
|
+
class << self
|
|
166
|
+
attr_accessor :factory_ir
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def initialize(overrides)
|
|
170
|
+
@overrides = overrides.empty? ? overrides : overrides.transform_keys(&:to_sym)
|
|
171
|
+
@cache = @overrides.empty? ? {} : @overrides.dup
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
attr_reader :instance
|
|
175
|
+
|
|
176
|
+
def association(name, *traits_and_overrides)
|
|
177
|
+
overrides = traits_and_overrides.last.is_a?(Hash) ? traits_and_overrides.pop.dup : {}
|
|
178
|
+
strategy = overrides.delete(:strategy)
|
|
179
|
+
strategy ||= ::FactoryBot.use_parent_strategy ? :build : :create
|
|
180
|
+
if %i[build create].include?(strategy)
|
|
181
|
+
return FactoryHoist.public_send(strategy, name, *traits_and_overrides, **overrides)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
::FactoryBot.public_send(strategy, name, *traits_and_overrides, **overrides)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def method_missing(name, ...)
|
|
188
|
+
return @instance.send(name, ...) if @instance.respond_to?(name)
|
|
189
|
+
return ::FactoryBot::SyntaxRunner.new.send(name, ...) if ::FactoryBot::SyntaxRunner.new.respond_to?(name)
|
|
190
|
+
|
|
191
|
+
super
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def respond_to_missing?(name, include_private = false)
|
|
195
|
+
@instance.respond_to?(name) || ::FactoryBot::SyntaxRunner.new.respond_to?(name) || super
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
private
|
|
199
|
+
|
|
200
|
+
def build_class
|
|
201
|
+
ir = self.class.factory_ir
|
|
202
|
+
return ir[:klass] if ir[:klass]
|
|
203
|
+
|
|
204
|
+
parts = ir[:class_name].split("::").reject(&:empty?)
|
|
205
|
+
return ir[:klass] = Object.const_get(parts.first, false) if parts.one?
|
|
206
|
+
|
|
207
|
+
ir[:klass] = parts.inject(Object) { |scope, constant| scope.const_get(constant, false) }
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FactoryHoist
|
|
4
|
+
module MinitestDSL
|
|
5
|
+
def hoist(name, factory = name, *traits, **attributes, &block)
|
|
6
|
+
name = name.to_sym
|
|
7
|
+
definitions = if instance_variable_defined?(:@factory_hoist_definitions)
|
|
8
|
+
@factory_hoist_definitions
|
|
9
|
+
else
|
|
10
|
+
@factory_hoist_definitions = {}
|
|
11
|
+
end
|
|
12
|
+
raise DuplicateHoistError, "hoist(:#{name}) is already declared in this class" if definitions.key?(name)
|
|
13
|
+
|
|
14
|
+
definition = Definition.new(name, factory, traits.freeze, attributes.freeze, block, to_s)
|
|
15
|
+
definitions[name] = definition
|
|
16
|
+
define_method(name) { factory_hoist_local_values.fetch(name) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def factory_hoist_definitions
|
|
20
|
+
inherited = superclass.respond_to?(:factory_hoist_definitions) ? superclass.factory_hoist_definitions : {}
|
|
21
|
+
inherited.merge(instance_variable_get(:@factory_hoist_definitions) || {})
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class MinitestValues
|
|
26
|
+
def initialize(test)
|
|
27
|
+
@test = test
|
|
28
|
+
@values = {}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def fetch(name)
|
|
32
|
+
FactoryHoist.stats.increment(:references)
|
|
33
|
+
return @values[name] if @values.key?(name)
|
|
34
|
+
|
|
35
|
+
definition = @test.class.factory_hoist_definitions.fetch(name)
|
|
36
|
+
FactoryHoist.stats.record_reference("#{definition.node_path} #{name}")
|
|
37
|
+
FactoryHoist.stats.increment(:deoptimizations)
|
|
38
|
+
@values[name] = definition.materialize(self)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
alias_method :[], :fetch
|
|
42
|
+
|
|
43
|
+
def __factory_hoist_evaluate__(&block)
|
|
44
|
+
@test.instance_exec(&block)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
48
|
+
if args.empty? && kwargs.empty? && @test.class.factory_hoist_definitions.key?(name)
|
|
49
|
+
return fetch(name)
|
|
50
|
+
end
|
|
51
|
+
return @test.__send__(name, *args, **kwargs, &block) if @test.respond_to?(name, true)
|
|
52
|
+
|
|
53
|
+
super
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def respond_to_missing?(name, include_private = false)
|
|
57
|
+
@test.class.factory_hoist_definitions.key?(name) || @test.respond_to?(name, true) || super
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
module MinitestInstance
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def factory_hoist_local_values
|
|
65
|
+
@factory_hoist_local_values ||= MinitestValues.new(self)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.install_minitest!
|
|
70
|
+
return unless defined?(::Minitest::Test)
|
|
71
|
+
return if ::Minitest::Test.singleton_class < MinitestDSL
|
|
72
|
+
|
|
73
|
+
::Minitest::Test.extend(MinitestDSL)
|
|
74
|
+
::Minitest::Test.include(MinitestInstance)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
FactoryHoist.install_minitest!
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FactoryHoist
|
|
4
|
+
module ParallelDatabase
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def clone(source:, target:, adapter:)
|
|
8
|
+
case adapter.respond_to?(:to_sym) && adapter.to_sym
|
|
9
|
+
when :postgresql then clone_postgresql(source, target)
|
|
10
|
+
when :sqlite then clone_sqlite(source, target)
|
|
11
|
+
else raise ArgumentError, "parallel database cloning is unsupported for #{adapter}"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def clone_sqlite(source, target)
|
|
16
|
+
require "sqlite3"
|
|
17
|
+
|
|
18
|
+
created = false
|
|
19
|
+
complete = false
|
|
20
|
+
mode = File.stat(source).mode & 0o777
|
|
21
|
+
File.open(target, File::WRONLY | File::CREAT | File::EXCL, 0o600).close
|
|
22
|
+
created = true
|
|
23
|
+
source_database = SQLite3::Database.new(source, readonly: true)
|
|
24
|
+
target_database = SQLite3::Database.new(target)
|
|
25
|
+
backup = SQLite3::Backup.new(target_database, "main", source_database, "main")
|
|
26
|
+
status = backup.step(-1)
|
|
27
|
+
unless status == SQLite3::Constants::ErrorCode::DONE
|
|
28
|
+
raise Error, "SQLite backup failed with status #{status}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
File.chmod(mode, target)
|
|
32
|
+
complete = true
|
|
33
|
+
target
|
|
34
|
+
rescue Errno::EEXIST
|
|
35
|
+
raise Error, "target database already exists: #{target}"
|
|
36
|
+
ensure
|
|
37
|
+
backup&.finish
|
|
38
|
+
target_database&.close
|
|
39
|
+
source_database&.close
|
|
40
|
+
File.unlink(target) if created && !complete && File.exist?(target)
|
|
41
|
+
end
|
|
42
|
+
private_class_method :clone_sqlite
|
|
43
|
+
|
|
44
|
+
def clone_postgresql(source_url, target)
|
|
45
|
+
require "pg"
|
|
46
|
+
valid_target = target.is_a?(String) && target.bytesize <= 63 && target.match?(/\A[a-zA-Z0-9_]+\z/)
|
|
47
|
+
raise ArgumentError, "invalid target database name" unless valid_target
|
|
48
|
+
|
|
49
|
+
source_connection = PG.connect(source_url)
|
|
50
|
+
parameters = source_connection.conninfo_hash.transform_keys(&:to_sym)
|
|
51
|
+
.reject { |_key, value| value.nil? || value.empty? }
|
|
52
|
+
source = parameters[:dbname]
|
|
53
|
+
source_connection.close
|
|
54
|
+
raise ArgumentError, "source URL must include a database" unless source
|
|
55
|
+
|
|
56
|
+
admin = PG.connect(parameters.merge(dbname: "postgres"))
|
|
57
|
+
admin.exec_params("SELECT pg_advisory_lock(hashtext($1))", ["factory_hoist_database_clone"])
|
|
58
|
+
ensure_cloneable!(admin, source, target)
|
|
59
|
+
admin.exec("CREATE DATABASE #{admin.quote_ident(target)} TEMPLATE #{admin.quote_ident(source)}")
|
|
60
|
+
target
|
|
61
|
+
ensure
|
|
62
|
+
source_connection&.close unless source_connection&.finished?
|
|
63
|
+
if admin && !admin.finished?
|
|
64
|
+
begin
|
|
65
|
+
admin.exec_params("SELECT pg_advisory_unlock(hashtext($1))", ["factory_hoist_database_clone"])
|
|
66
|
+
rescue PG::Error
|
|
67
|
+
nil
|
|
68
|
+
ensure
|
|
69
|
+
admin.close
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
private_class_method :clone_postgresql
|
|
74
|
+
|
|
75
|
+
def ensure_cloneable!(connection, source, target)
|
|
76
|
+
exists = connection.exec_params("SELECT 1 FROM pg_database WHERE datname = $1", [target]).ntuples.positive?
|
|
77
|
+
raise Error, "target database already exists: #{target}" if exists
|
|
78
|
+
|
|
79
|
+
connections = connection.exec_params(
|
|
80
|
+
"SELECT count(*) FROM pg_stat_activity WHERE datname = $1",
|
|
81
|
+
[source]
|
|
82
|
+
).getvalue(0, 0).to_i
|
|
83
|
+
raise Error, "source database has #{connections} active connection(s): #{source}" if connections.positive?
|
|
84
|
+
end
|
|
85
|
+
private_class_method :ensure_cloneable!
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FactoryHoist
|
|
4
|
+
class PCG32
|
|
5
|
+
MULTIPLIER = 6_364_136_223_846_793_005
|
|
6
|
+
MASK_64 = (1 << 64) - 1
|
|
7
|
+
|
|
8
|
+
def initialize(seed, stream = 1)
|
|
9
|
+
@state = 0
|
|
10
|
+
@increment = ((stream << 1) | 1) & MASK_64
|
|
11
|
+
next_uint32
|
|
12
|
+
@state = (@state + seed) & MASK_64
|
|
13
|
+
next_uint32
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def rand(limit = nil)
|
|
17
|
+
return next_uint32.fdiv(1 << 32) unless limit
|
|
18
|
+
return integer(limit) if limit.is_a?(Integer)
|
|
19
|
+
return range(limit) if limit.is_a?(Range)
|
|
20
|
+
return rand * limit if limit.is_a?(Numeric) && limit.positive?
|
|
21
|
+
|
|
22
|
+
raise ArgumentError, "unsupported random limit: #{limit.inspect}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def bytes(length)
|
|
26
|
+
Array.new(length) { rand(256) }.pack("C*")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def integer(limit)
|
|
32
|
+
raise ArgumentError, "random limit must be positive: #{limit}" unless limit.positive?
|
|
33
|
+
return 0 if limit == 1
|
|
34
|
+
|
|
35
|
+
bits = (limit - 1).bit_length
|
|
36
|
+
words = (bits + 31) / 32
|
|
37
|
+
mask = (1 << bits) - 1
|
|
38
|
+
loop do
|
|
39
|
+
value = 0
|
|
40
|
+
words.times { value = (value << 32) | next_uint32 }
|
|
41
|
+
value &= mask
|
|
42
|
+
return value if value < limit
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def range(value)
|
|
47
|
+
first = value.begin
|
|
48
|
+
last = value.end
|
|
49
|
+
if first.is_a?(Integer) && last.is_a?(Integer)
|
|
50
|
+
size = last - first + (value.exclude_end? ? 0 : 1)
|
|
51
|
+
return first + integer(size)
|
|
52
|
+
end
|
|
53
|
+
if defined?(::Date) && first.instance_of?(::Date) && last.instance_of?(::Date)
|
|
54
|
+
size = (last - first).to_i + (value.exclude_end? ? 0 : 1)
|
|
55
|
+
return first + integer(size)
|
|
56
|
+
end
|
|
57
|
+
return first if first == last && !value.exclude_end?
|
|
58
|
+
|
|
59
|
+
size = last - first
|
|
60
|
+
raise ArgumentError, "non-numeric range distance: #{value.inspect}" unless size.is_a?(Numeric)
|
|
61
|
+
raise ArgumentError, "empty random range: #{value.inspect}" unless size.positive?
|
|
62
|
+
|
|
63
|
+
first + (rand * size)
|
|
64
|
+
rescue NoMethodError
|
|
65
|
+
raise ArgumentError, "unsupported random range: #{value.inspect}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def next_uint32
|
|
69
|
+
old_state = @state
|
|
70
|
+
@state = (old_state * MULTIPLIER + @increment) & MASK_64
|
|
71
|
+
xor_shifted = (((old_state >> 18) ^ old_state) >> 27) & 0xffffffff
|
|
72
|
+
rotation = old_state >> 59
|
|
73
|
+
((xor_shifted >> rotation) | (xor_shifted << ((-rotation) & 31))) & 0xffffffff
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "compatibility"
|
|
4
|
+
require_relative "scheduler"
|
|
5
|
+
|
|
6
|
+
module FactoryHoist
|
|
7
|
+
module RSpecDSL
|
|
8
|
+
def hoist(name, factory = name, *traits, **attributes, &block)
|
|
9
|
+
name = name.to_sym
|
|
10
|
+
definitions = if instance_variable_defined?(:@factory_hoist_definitions)
|
|
11
|
+
@factory_hoist_definitions
|
|
12
|
+
else
|
|
13
|
+
@factory_hoist_definitions = {}
|
|
14
|
+
end
|
|
15
|
+
raise DuplicateHoistError, "hoist(:#{name}) is already declared in this group" if definitions.key?(name)
|
|
16
|
+
|
|
17
|
+
node_path = metadata[:full_description]
|
|
18
|
+
definition = Definition.new(name, factory, traits.freeze, attributes.freeze, block, node_path)
|
|
19
|
+
definitions[name] = definition
|
|
20
|
+
define_method(name) do
|
|
21
|
+
Runtime.current.fetch(self, name, definition, Scheduler.definitions_for(self.class))
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
::RSpec::Core::ExampleGroup.extend(RSpecDSL)
|
|
27
|
+
::RSpec.configure do |config|
|
|
28
|
+
config.before(:suite) do
|
|
29
|
+
Compatibility.warn_for_database_cleaner
|
|
30
|
+
Scheduler.install!
|
|
31
|
+
end
|
|
32
|
+
config.around(:each) do |example|
|
|
33
|
+
local = !Scheduler.definitions_for(example.example_group).empty?
|
|
34
|
+
Runtime.current.around_example(example, local: local)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|