consistent_random 2.1.1 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +31 -0
- data/VERSION +1 -1
- data/lib/consistent_random/testing.rb +169 -0
- data/lib/consistent_random.rb +23 -5
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 87b396dd6b089d0fb4c99777531eb0824a4d78b2180bce9eeee6484efd06ddbd
|
4
|
+
data.tar.gz: f5ade642f1222468d68ca326cfad4a206b9e74b97fe786e2497d13e9a67a0459
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8ab235ba4d2b6b447e05614cd603c739461e1d3e0efad6c088ff07b966b6daf207f76c413a49dc00d78125ee12f1ca252b91cba01154d00441bc03dd00523628
|
7
|
+
data.tar.gz: 4a5ca92427978a6f496bc75b057c6982bf7a9eac870eef9139ab5e1cce36aab236c2da6acaf50b551d483d696ce6235d063ba4883a37110569b8f4395571e5e7
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
|
+
## 2.2.0
|
8
|
+
|
9
|
+
### Added
|
10
|
+
|
11
|
+
- Added `ConsistentRandom.testing` method to allow for deterministic testing of random values.
|
12
|
+
- Added `ConsistentRandom#name` method to return the name used for seeding the random value.
|
13
|
+
|
7
14
|
## 2.1.1
|
8
15
|
|
9
16
|
### Fixed
|
data/README.md
CHANGED
@@ -18,6 +18,7 @@ For example, consider rolling out a new feature to a subset of requests. You may
|
|
18
18
|
- [Rack Middleware](#rack-middleware)
|
19
19
|
- [Sidekiq Middleware](#sidekiq-middleware)
|
20
20
|
- [ActiveJob](#activejob)
|
21
|
+
- [Testing](#testing)
|
21
22
|
- [Installation](#installation)
|
22
23
|
- [Contributing](#contributing)
|
23
24
|
- [License](#license)
|
@@ -206,6 +207,36 @@ class MyJob < ApplicationJob
|
|
206
207
|
end
|
207
208
|
```
|
208
209
|
|
210
|
+
## Testing
|
211
|
+
|
212
|
+
The gem provides a `ConsistentRandom.testing` method to allow for deterministic testing of random values. This method can be used to set fixed values within the block so that your tests will produce consistent results.
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
# Specify that all random values should be 0.5
|
216
|
+
ConsistentRandom.testing.rand(0.5) do
|
217
|
+
expect(ConsistentRandom.new("foo").rand).to eq(0.5)
|
218
|
+
expect(ConsistentRandom.new("bar").rand).to eq(0.5)
|
219
|
+
|
220
|
+
# The rand value must be between 0 and 1, but it will be scaled to fit
|
221
|
+
# any size or range specified for `rand`.
|
222
|
+
expect(ConsistentRandom.new("foo").rand(10)).to eq(5)
|
223
|
+
end
|
224
|
+
|
225
|
+
# You can also specify values for specific names.
|
226
|
+
# If a values isn't specified, it will return a random value.
|
227
|
+
ConsistentRandom.testing(foo: 0.5, bar: 0.8) do
|
228
|
+
expect(ConsistentRandom.new("foo").rand).to eq(0.5)
|
229
|
+
expect(ConsistentRandom.new("bar").rand).to eq(0.8)
|
230
|
+
end
|
231
|
+
|
232
|
+
# You can also specify values for the `bytes` and `seed` methods. The methods
|
233
|
+
# for setting test valus can be chained together.
|
234
|
+
ConsistentRandom.testing.bytes(foo: "bar").seed(baz: 123) do
|
235
|
+
expect(ConsistentRandom.new("foo").bytes(6)).to eq("barbar")
|
236
|
+
expect(ConsistentRandom.new("baz").seed).to eq(123)
|
237
|
+
end
|
238
|
+
```
|
239
|
+
|
209
240
|
## Installation
|
210
241
|
|
211
242
|
Add this line to your application's Gemfile:
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.2.0
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ConsistentRandom
|
4
|
+
# This class returns an object that can be used to generate deterministic values
|
5
|
+
# for use in testing.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# ConsistentRandom.testing.rand(foo: 0.5).bytes(foo: "foobar").seed(123) do
|
9
|
+
# expect(ConsistentRandom.new("foo").rand).to eq(0.5)
|
10
|
+
# expect(ConsistentRandom.new("bar").rand).not_to eq(0.5)
|
11
|
+
#
|
12
|
+
# expect(ConsistentRandom.new("foo").bytes(12)).to eq("foobarfoobar")
|
13
|
+
#
|
14
|
+
# expect(ConsistentRandom.new("foo").seed).to eq(123)
|
15
|
+
# end
|
16
|
+
class ConsistentRandom::Testing
|
17
|
+
class << self
|
18
|
+
# Get the testing object if any for the current block.
|
19
|
+
#
|
20
|
+
# @return [ConsistentRandom::Testing, nil] the testing object or nil if not set
|
21
|
+
# @api private
|
22
|
+
def current
|
23
|
+
Thread.current[:consistent_random_testing]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
@rand_hash = {}
|
29
|
+
@bytes_hash = {}
|
30
|
+
@seed_hash = {}
|
31
|
+
end
|
32
|
+
|
33
|
+
# Set the random value returned by ConsistentRandom#rand.
|
34
|
+
#
|
35
|
+
# param options [Float, Hash<String, Float>] the value to return for rand. If a Float is given
|
36
|
+
# then it will be used as the value for all calls to rand. If a Hash is given then the value
|
37
|
+
# will only be returned for ConsitentRandom objects with the names specified in the keys.
|
38
|
+
# @yield block of code to execute with the test values.
|
39
|
+
# @return [ConsistentRandom::Testing, Object] If a block is specified, then the result
|
40
|
+
# of the block is returned. Otherwise the testing object is returned so that you can
|
41
|
+
# chain calls to set up more test values.
|
42
|
+
def rand(options, &block)
|
43
|
+
options = validate_rand(options)
|
44
|
+
unless options
|
45
|
+
raise ArgumentError.new("Argument must be a Float between 0 and 1 or a Hash with Float values")
|
46
|
+
end
|
47
|
+
|
48
|
+
@rand_hash = options.default ? options.merge(@rand_hash) : @rand_hash.merge(options)
|
49
|
+
if block
|
50
|
+
use(&block)
|
51
|
+
else
|
52
|
+
self
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Set the random bytes returned by ConsistentRandom#bytes.
|
57
|
+
#
|
58
|
+
# param options [String, Hash<String, String>] the value to return for bytes. If a String is given
|
59
|
+
# then it will be used as the value for all calls to bytes. If a Hash is given then the value
|
60
|
+
# will only be returned for ConsitentRandom objects with the names specified in the keys.
|
61
|
+
# @yield block of code to execute with the test values.
|
62
|
+
# @return [ConsistentRandom::Testing, Object] If a block is specified, then the result
|
63
|
+
# of the block is returned. Otherwise the testing object is returned so that you can
|
64
|
+
# chain calls to set up more test values.
|
65
|
+
def bytes(options, &block)
|
66
|
+
options = validate_bytes(options)
|
67
|
+
unless options
|
68
|
+
raise ArgumentError.new("Argument must be a String or a Hash with String values")
|
69
|
+
end
|
70
|
+
|
71
|
+
@bytes_hash = options.default ? options.merge(@bytes_hash) : @bytes_hash.merge(options)
|
72
|
+
if block
|
73
|
+
use(&block)
|
74
|
+
else
|
75
|
+
self
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Set the seed value returned by ConsistentRandom#seed.
|
80
|
+
#
|
81
|
+
# param options [Integer, Hash<String, Integer>] the value to return for seed. If an Integer is given
|
82
|
+
# then it will be used as the value for all calls to seed. If a Hash is given then the value
|
83
|
+
# will only be returned for ConsitentRandom objects with the names specified in the keys.
|
84
|
+
# @yield block of code to execute with the test values.
|
85
|
+
# @return [ConsistentRandom::Testing, Object] If a block is specified, then the result
|
86
|
+
# of the block is returned. Otherwise the testing object is returned so that you can
|
87
|
+
# chain calls to set up more test
|
88
|
+
def seed(options, &block)
|
89
|
+
options = validate_seed(options)
|
90
|
+
unless options
|
91
|
+
raise ArgumentError.new("Argument must be an Integer or a Hash with Integer values")
|
92
|
+
end
|
93
|
+
|
94
|
+
@seed_hash = options.default ? options.merge(@seed_hash) : @seed_hash.merge(options)
|
95
|
+
if block
|
96
|
+
use(&block)
|
97
|
+
else
|
98
|
+
self
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Use the test values within a block of code.
|
103
|
+
#
|
104
|
+
# @yield block of code to execute with the test values.
|
105
|
+
# @return [Object] the result of the block
|
106
|
+
def use(&block)
|
107
|
+
save_val = Thread.current[:consistent_random_testing]
|
108
|
+
begin
|
109
|
+
Thread.current[:consistent_random_testing] = self
|
110
|
+
yield
|
111
|
+
ensure
|
112
|
+
Thread.current[:consistent_random_testing] = save_val
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Get the test value for rand.
|
117
|
+
#
|
118
|
+
# @param name [String] the name of the ConsistentRandom object
|
119
|
+
# @return [Float, nil] the test value for rand if there is a test value for the name
|
120
|
+
# @api private
|
121
|
+
def rand_for(name)
|
122
|
+
@rand_hash[name]
|
123
|
+
end
|
124
|
+
|
125
|
+
# Get the test value for bytes.
|
126
|
+
#
|
127
|
+
# @param name [String] the name of the ConsistentRandom object
|
128
|
+
# @return [String, nil] the test value for bytes if there is a test value for the name
|
129
|
+
# @api private
|
130
|
+
def bytes_for(name)
|
131
|
+
@bytes_hash[name]
|
132
|
+
end
|
133
|
+
|
134
|
+
# Get the test value for seed.
|
135
|
+
#
|
136
|
+
# @param name [String] the name of the ConsistentRandom object
|
137
|
+
# @return [Integer, nil] the test value for seed if there is a test value for the name
|
138
|
+
# @api private
|
139
|
+
def seed_for(name)
|
140
|
+
@seed_hash[name]
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def validate_rand(options)
|
146
|
+
if options.is_a?(Hash) && options.values.all? { |value| value.is_a?(Float) && (0...1).cover?(value) }
|
147
|
+
options.each_with_object({}) { |(key, value), hash| hash[key.to_s] = value }
|
148
|
+
elsif options.is_a?(Float) && (0...1).cover?(options)
|
149
|
+
Hash.new(options)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def validate_bytes(options)
|
154
|
+
if options.is_a?(Hash) && options.values.all? { |value| value.is_a?(String) }
|
155
|
+
options.each_with_object({}) { |(key, value), hash| hash[key.to_s] = value.encode(Encoding::ASCII_8BIT) }
|
156
|
+
elsif options.is_a?(String)
|
157
|
+
Hash.new(options)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def validate_seed(options)
|
162
|
+
if options.is_a?(Hash) && options.values.all? { |value| value.is_a?(Integer) }
|
163
|
+
options.each_with_object({}) { |(key, value), hash| hash[key.to_s] = value }
|
164
|
+
elsif options.is_a?(Integer)
|
165
|
+
Hash.new(options)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
data/lib/consistent_random.rb
CHANGED
@@ -51,11 +51,17 @@ class ConsistentRandom
|
|
51
51
|
def current_seed
|
52
52
|
Thread.current[:consistent_random_seed]
|
53
53
|
end
|
54
|
+
|
55
|
+
def testing
|
56
|
+
Testing.new
|
57
|
+
end
|
54
58
|
end
|
55
59
|
|
60
|
+
attr_reader :name
|
61
|
+
|
56
62
|
# @param name [Object] a name used to identifuy a consistent random value
|
57
63
|
def initialize(name)
|
58
|
-
@name = name
|
64
|
+
@name = (name.is_a?(String) ? name.dup : name.to_s).freeze
|
59
65
|
end
|
60
66
|
|
61
67
|
# Generate a random number. The same number will be generated within a scope block.
|
@@ -68,7 +74,7 @@ class ConsistentRandom
|
|
68
74
|
# a number in that range. If max is an number, then it will be an integer between 0 and that
|
69
75
|
# value. Otherwise, it will be a float between 0 and 1.
|
70
76
|
def rand(max = nil)
|
71
|
-
value = seed / SEED_DIVISOR
|
77
|
+
value = Testing.current&.rand_for(name) || seed / SEED_DIVISOR
|
72
78
|
case max
|
73
79
|
when nil
|
74
80
|
value
|
@@ -85,8 +91,14 @@ class ConsistentRandom
|
|
85
91
|
# @param size [Integer] the number of bytes to generate.
|
86
92
|
# @return [String] a string of random bytes.
|
87
93
|
def bytes(size)
|
94
|
+
test_bytes = Testing.current&.bytes_for(name)
|
95
|
+
test_bytes = nil if test_bytes&.empty?
|
96
|
+
chunk_size = (test_bytes ? test_bytes.length : 20)
|
97
|
+
|
88
98
|
bytes = []
|
89
|
-
(
|
99
|
+
(size / chunk_size.to_f).ceil.times do |i|
|
100
|
+
bytes << (test_bytes || seed_hash("#{name}#{i}").to_s)
|
101
|
+
end
|
90
102
|
bytes.join[0, size]
|
91
103
|
end
|
92
104
|
|
@@ -95,13 +107,17 @@ class ConsistentRandom
|
|
95
107
|
#
|
96
108
|
# @return [Integer] a 64 bit integer for seeding random values
|
97
109
|
def seed
|
98
|
-
|
110
|
+
test_seed = Testing.current&.seed_for(name)
|
111
|
+
return test_seed if test_seed
|
112
|
+
|
113
|
+
hash = seed_hash(name)
|
99
114
|
hash.byteslice(0, 8).unpack1("Q>")
|
100
115
|
end
|
101
116
|
|
102
117
|
# @return [Boolean] true if the other object is a ConsistentRandom that returns
|
103
118
|
# the same random number generator. If called outside of a scope, then it will
|
104
|
-
# always return false.
|
119
|
+
# always return false. The functionality is designed to be similar to the
|
120
|
+
# same behavior as Random.
|
105
121
|
def ==(other)
|
106
122
|
other.is_a?(self.class) && other.seed == seed
|
107
123
|
end
|
@@ -138,3 +154,5 @@ class ConsistentRandom
|
|
138
154
|
int_range ? val.to_i : val
|
139
155
|
end
|
140
156
|
end
|
157
|
+
|
158
|
+
require_relative "consistent_random/testing"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: consistent_random
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brian Durand
|
@@ -41,6 +41,7 @@ files:
|
|
41
41
|
- lib/consistent_random/rack_middleware.rb
|
42
42
|
- lib/consistent_random/sidekiq_client_middleware.rb
|
43
43
|
- lib/consistent_random/sidekiq_middleware.rb
|
44
|
+
- lib/consistent_random/testing.rb
|
44
45
|
homepage: https://github.com/bdurand/consistent_random
|
45
46
|
licenses:
|
46
47
|
- MIT
|