consistent_random 2.1.0 → 2.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 +13 -0
- data/README.md +31 -0
- data/VERSION +1 -1
- data/lib/consistent_random/testing.rb +169 -0
- data/lib/consistent_random.rb +24 -6
- metadata +3 -2
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,19 @@ 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
|
+
|
14
|
+
## 2.1.1
|
15
|
+
|
16
|
+
### Fixed
|
17
|
+
|
18
|
+
- Fixed typo in `==` method which caused a NoMethodError when comparing ConsistentRandom instances.
|
19
|
+
|
7
20
|
## 2.1.0
|
8
21
|
|
9
22
|
### Added
|
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,15 +107,19 @@ 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
|
-
other.is_a?(self.class) && other.seed
|
122
|
+
other.is_a?(self.class) && other.seed == seed
|
107
123
|
end
|
108
124
|
|
109
125
|
# Generate a random number generator for the given name. The generator will always
|
@@ -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,14 +1,14 @@
|
|
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
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-11-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -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
|